QSettings、QApplication相关的 BUG 一例

本文通过一个简单的Qt程序示例,揭示了一个导致程序崩溃的微妙bug。问题源于QApplication析构过程中GUI类型的不当注销,影响到QSettings的操作。文章详细分析了问题原因,并探讨了使用QCoreApplication代替QApplication的解决方案。

问题重现

看看下面这个简单的程序,能猜出会发生什么问题么?

#include <QtCore/QSettings>
#include <QtGui/QApplication>
#include <QtGui/QColor>

class A:public QObject
{
public:
    A(QObject *parent):QObject(parent){}
    ~A()
    {
        QSettings settings("test.ini", QSettings::IniFormat);
        settings.setValue("color", QColor(Qt::red));
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    A * a = new A(&app);
    return 0;
}

该程序退出时会崩溃。今天几乎用了一天的时间来定位这个bug,不过根本原因总算找到了。并可用前面的代码进行重现。

原因

QApplication 构造与析构时会注册与反注册掉GUI相关的 Variant 类型(Font、Color等等)。

QApplication::QApplication(int &argc, char **argv)
{
...
    // trigger registering of QVariant's GUI types
    qRegisterGuiVariant();
...
}

QApplication::~QApplication()
{
...
    // trigger unregistering of QVariant's GUI types
    qUnregisterGuiVariant();
}

QSetttings 将 QFont、QColor 等写入配置文件时需要这些信息,其实不止是QSettings,任何需要调用QMetaType::save的都有这个问题,将QVariant的的数据写入QDataStream流。

bool QMetaType::save(QDataStream &stream, int type, const void *data)
{
...
    switch(type) {
...
    case QMetaType::Long:
        stream << qlonglong(*static_cast<const long *>(data));
        break;
    case QMetaType::Int:
        stream << *static_cast<const int *>(data);
        break;
...
    case QMetaType::QFont:
    case QMetaType::QPixmap:
    case QMetaType::QBrush:
    case QMetaType::QColor:
    case QMetaType::QPalette:
...
    case QMetaType::QQuaternion:
        if (!qMetaTypeGuiHelper)
            return false;
        qMetaTypeGuiHelper[type - FirstGuiType].saveOp(stream, data);
        break;
}

出现问题的原因: a的parent是QApplication对象 app,所以 app 析构到最后时时将自动 delete 掉 a。此处a的析构函数被调用,QSettings 被激活。但是,在析构a之前,QAcpplication析构函数中已经将qMetaTypeGuiHelper置位0。于是,悲剧了

疑问?

既然 QApplication 负责注册与反注册,可是为什么,为什么? 如果我们前面的代码中不使用 QApplication 而使用 QCoreApplication 的话,却不会出错,这又是为何??

看看前面调用的两个函数:

源码文件:qguivariant.cpp

int qRegisterGuiVariant()
{
...
    qMetaTypeGuiHelper = qVariantGuiHelper;
    return 1;
}
Q_CONSTRUCTOR_FUNCTION(qRegisterGuiVariant)

int qUnregisterGuiVariant()
{
...
    qMetaTypeGuiHelper = 0;
    return 1;
}
Q_DESTRUCTOR_FUNCTION(qUnregisterGuiVariant)

函数很简单,不简单之处在于,此处多了两个宏:

# define Q_CONSTRUCTOR_FUNCTION0(AFUNC) \
   static const int AFUNC ## __init_variable__ = AFUNC();
# define Q_CONSTRUCTOR_FUNCTION(AFUNC) Q_CONSTRUCTOR_FUNCTION0(AFUNC)

# define Q_DESTRUCTOR_FUNCTION0(AFUNC) \
    class AFUNC ## __dest_class__ { \
    public: \
       inline AFUNC ## __dest_class__() { } \
       inline ~ AFUNC ## __dest_class__() { AFUNC(); } \
    } AFUNC ## __dest_instance__;
# define Q_DESTRUCTOR_FUNCTION(AFUNC) Q_DESTRUCTOR_FUNCTION0(AFUNC)

似乎有点乱,我们展开看一眼:

static const int qRegisterGuiVariant__init_variable__ = qRegisterGuiVariant();

class qUnregisterGuiVariant__dest_class__ {
public: \
    inline qUnregisterGuiVariant__dest_class__() { }
    inline ~ qUnregisterGuiVariant__dest_class__() { qUnregisterGuiVariant(); }
} qUnregisterGuiVariant__dest_instance__;

一切很明了,

  • 构造一个static全局变量,编译器会强制 qRegisterGuiVariant() 在 main 函数之前被执行。
  • 构造另一个全局对象,程序退出时,其析构函数被执行,进而调用 qUnregisterGuiVariant();

这样看来,QApplication 中的动作反而有点多此一举,而且提前调用了一次qUnregisterGuiVariant(),还导致我们前面的问题。(当然,官方这样应该有自己的理由,只是我们尚不太清楚罢了)

小记

程序中使用多个dll动态库,而且用了有点Qt特色的单例模式,结果导致了bug定位相当困难。

不过呢,收获似乎还不错。


我在实行PyQt5时,出现主界面,但是点击按钮没有显示图片,下面是我的代码 import sys from PyQt5.QtWidgets import QApplication,QMainWindow,QDialog,QMessageBox,QGridLayout from PyQt5.QtCore import Qt from Database import * from PyQt5 import QtCore import prc7 #主界面 import Main_wiond #登陆注册界面 from PyQt5.QtCore import QSettings from matplotlib import pyplot as plt import matplotlib #避免中文乱码 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['font.size'] = 20 #设置整体字体大小 plt.rcParams['axes.unicode_minus'] = False #从matplotlib 导入与Qt集成相关的类 matplotlib.use("Qt5Agg") #强制使用Qt5后端进行渲染 from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class MyFigure(FigureCanvas): def __init__(self,width=5,height=4,dpi=100): #第一步,创建一个Figure facecolor背景色 self.fig=Figure(figsize=(width,height),dpi=dpi,facecolor="#458cff") #第二部,激活Figure窗口,这一步不能少,否则不显示图形 super(MyFigure,self).__init__(self.fig) class Login_Register(QMainWindow): def __init__(self): super().__init__() #初始化父类的属性 self.ui=Main_wiond.Ui_MainWindow() #实例化登陆界面窗口类对象 self.win = QMainWindow() #窗口对象 self.win.setWindowFlags(Qt.FramelessWindowHint)#去除边框 self.win.setAttribute(Qt.WA_TranslucentBackground) self.ui.setupUi(self.win) #这个语法取决于你的窗口里面函数定义格式 self.ui.zhuce.hide()#初始化隐藏你的注册界面 self.loadsetings()# self.win.show() def D_change_z(self): #登录切换注册 self.ui.denglu.hide() self.ui.zhuce.show() def Z_change_D(self): #注册切换登录 self.ui.denglu.show() self.ui.zhuce.hide() def close_win(self):#关闭窗口 self.ui.pushButton_3.clicked.connect(self.win.close)#关闭符合按钮的对象名称 def loadsetings(self):#加载保存账号密码 setting = QSettings("admin", "lyjd") #读取记住密码状态,默认为False,setting.value(key,defaultvalue默认值,type) remember = setting.value("remember",False,type=bool) self.ui.checkBox.setChecked(remember)#选中状态 if remember: username = setting.value("username","",type=str) self.ui.lineEdit.setText(username) #同样也可以加载密码 password = setting.value("password","",type=str) self.ui.lineEdit_2.setText(password) def savesetings(self):#保存用户名和密码 setting = QSettings("admin","lyjd") #保存记住密码的状态 remember = self.ui.checkBox.isChecked() setting.setValue("remember",remember) #保存用户名密码 if remember: user = self.ui.lineEdit.text() #用户名 setting.setValue("username",user) #如果不需要记住密码,可以注释下方代码 password = self.ui.lineEdit_2.text()#密码 setting.setValue("password", password) else: #如果不记住,可以删除保存的账户密码 setting.remove("username") setting.remove("password") def login_user(self):#登录验证 user = self.ui.lineEdit.text() password = self.ui.lineEdit_2.text() self.savesetings()#保存设置(无论登录成功与否都会保存记住密码选择) if password_check_sql(user):#验证账号是否存在 data=password_check_sql(user) if password==data[0].get("password"):#获取账号对应的密码 QMessageBox.information(self.win,"提示","登录成功") #切换主界面 self.home=Home() self.win.hide() else: QMessageBox.critical(self.win,"错误","登录失败") else: QMessageBox.critical(self.win,"错误","账号不存在") def res_user(self):#注册验证 user = self.ui.lineEdit_3.text() password = self.ui.lineEdit_4.text() check_password = self.ui.lineEdit_5.text() if not user or not password or not check_password: QMessageBox.warning(self.win, "警告","用户名和密码不能为空!") return if password == check_password: if password_check_sql(user):#验证账号是否存在 QMessageBox.critical(self.win,"错误","账号已经存在") else: res_logon(user,password) QMessageBox.information(self.win,"提示框","注册成功") #清空输入框 self.ui.lineEdit_3.clear() self.ui.lineEdit_4.clear() self.ui.lineEdit_5.clear() self.Z_change_D()#注册成功切换登录页面 else: QMessageBox.critical(self.win,"错误","两次密码不一样") class Home(QMainWindow): def __init__(self): super(Home, self).__init__() self.win=QMainWindow() self.ui=prc7.Ui_MainWindow() #主界面窗口对象 self.ui.setupUi(self.win) self.win.show() self.ui.pushButton.clicked.connect(self.change_groupbox) self.ui.pushButton_2.clicked.connect(self.change_groupbox_2) self.ui.pushButton_3.clicked.connect(self.change_groupbox_3) self.ui.pushButton_4.clicked.connect(self.change_groupbox_4) self.ui.pushButton_5.clicked.connect(self.change_groupbox_5) def change_groupbox(self): self.ui.groupBox.show() self.ui.groupBox_2.hide() self.ui.groupBox_3.hide() self.ui.groupBox_4.hide() self.ui.groupBox_5.hide() self.draw_1() def change_groupbox_2(self): self.ui.groupBox.hide() self.ui.groupBox_2.show() self.ui.groupBox_3.hide() self.ui.groupBox_4.hide() self.ui.groupBox_5.hide() self.draw_2() def change_groupbox_3(self): self.ui.groupBox.hide() self.ui.groupBox_2.hide() self.ui.groupBox_3.show() self.ui.groupBox_4.hide() self.ui.groupBox_5.hide() self.draw_3() def change_groupbox_4(self): self.ui.groupBox.hide() self.ui.groupBox_2.hide() self.ui.groupBox_3.hide() self.ui.groupBox_4.show() self.ui.groupBox_5.hide() self.draw_4() def change_groupbox_5(self): self.ui.groupBox.hide() self.ui.groupBox_2.hide() self.ui.groupBox_3.hide() self.ui.groupBox_4.hide() self.ui.groupBox_5.show() self.draw_5() def draw_1(self):#直方图 try: data=Db_R('rating_distribution') print("评分分布数据:", data) if data.empty: print("没有获取到评分分布数据") return x=list(data.iloc[:,0])#评分 y=list(data.iloc[:,1])#频次 F=MyFigure(5,4,100) axes=F.add_subplot(111) axes.bar(x,y,color="#ffffff") #设置坐标名称 axes.set_xlabel("频次", color="w") axes.set_ylabel("评分",color="w") F.fig.suptitle("贵阳旅游景点评分分析",color="w",fontsize=28)#标题 QGridLayout(self.ui.groupBox).addWidget(F) except Exception as e: print(f"绘制评分分布直方图时出错: {e}") def draw_2(self):#饼图 data = Db_R("price_level_distribution") labels = list(data.iloc[:, 0]) # 价格等级 sizes = list(data.iloc[:, 1]) # 数量 F = MyFigure(5, 4, 100) axes = F.add_subplot(111) axes.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90) axes.axis('equal') # 保证饼图是圆的 F.fig.suptitle("价格等级分析", color="w", fontsize=30) # 标题 QGridLayout(self.ui.groupBox_2).addWidget(F) def draw_3(self):#箱线图 data = Db_R("boxplot_statistics") F = MyFigure(5, 4, 100) axes = F.add_subplot(111) F.fig.suptitle("旅游景点评分和价格分析", color="w", fontsize=20) # 标题 QGridLayout(self.ui.groupBox_3).addWidget(F) def draw_4(self): # 散点图 data = Db_R('price_rating_relation') x = list(data.iloc[:, 0]) #价格 y = list(data.iloc[:, 1]) # 评分 F = MyFigure(5, 4, 100) axes = F.add_subplot(111) axes.scatter(x, y, color="#ffffff") # 设置坐标名称 axes.set_xlabel("价格(元)", color="w") axes.set_ylabel("评分", color="w") F.fig.suptitle("景点价格与评分关系分析", color="w", fontsize=28) # 标题 QGridLayout(self.ui.groupBox_4).addWidget(F) def draw_5(self):#条形图 data=Db_R('top10_price') x=list(data.iloc[:,0])#价格 y=list(data.iloc[:,1])#景点名称 F=MyFigure(5,4,100) axes=F.add_subplot(111) axes.bar(x,y,color="#ffffff") #设置坐标名称 axes.set_xlabel("价格(元)", color="w") axes.set_ylabel("景点名称",color="w") F.fig.suptitle("贵阳景点价格TOP10排行榜",color="w",fontsize=28)#标题 QGridLayout(self.ui.groupBox_5).addWidget(F) if __name__=='__main__': QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)#做高清屏幕适配 app = QApplication(sys.argv) lyjd = Login_Register() lyjd.ui.pushButton_dl_2.clicked.connect(lyjd.D_change_z)#登录按钮链接注册切换登录函数,函数后括号不要 lyjd.ui.pushButton_dl_3.clicked.connect(lyjd.Z_change_D) lyjd.ui.pushButton_2.clicked.connect(lyjd.res_user) #注册 lyjd.ui.pushButton.clicked.connect(lyjd.login_user)#登录 lyjd.close_win() sys.exit(app.exec_())
最新发布
10-10
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值