Qt信号槽架构中不需要的无限循环问题

7

我遇到了qt信号槽系统的问题。

首先,我创建了一个名为System的类,采用单例模式,这样我可以在需要时访问它的实例。System有一个名为SelectionChanged的信号。

我有一个列表小部件,我正在将其itemSelectionChanged信号连接到我的自定义槽onSelectionChanged上。在onSelectionChanged槽中,我发出了System的SelectionChanged信号。目前还没有任何问题。

在我的软件设计中,对象的选择可以被许多GUI小部件或自定义类使用,并且System的SelectionChanged信号可以由除列表小部件之外的小部件发出。

因此,我在列表小部件中创建了一个名为OnSystemSelectionChanged的槽,然后将其连接到System的SelectionChanged信号。OnSystemSelectionChanged槽如下所示。

void MyListWidget::OnSystemSelectionChanged(QObject *sender)
{
    if (sender == this) return;
    // Then I want to get a list of selected objects and set them as selection of this widget like this:
    this->SetSelection(System::Instance()->GetSelectedObjects());
}

但问题在于当我开始设置列表小部件的选定项时,它会发出itemSelectionChanged信号,我的onSelectionChanged插槽将被调用。然后插槽将发出System的SelectionChanged信号,然后OnSystemSelectionChanged也将被调用。它将通过sender参数停止,但没有一种方法可以一次性设置列表小部件的选定项。
我该如何解决这个问题?
希望我已经很好地解释了我的问题。提前致谢。
编辑:拼写和语法错误已经纠正。

和BartoszKP的第二个解决方案类似,我也找到了一个解决方案。QObject有一个blockSignals()方法可以防止发射内部信号。 - Cahit Burak Küçüksütcü
4个回答

12

在Qt中,有几种处理此类情况的方法。

成语

  1. 使用一个底层模型与多个视图控件配合。这样可以自动处理对多个视图控件的更改传播,无需任何额外操作。您可以使用QDataWidgetMapper将“普通”的小部件链接到模型中的数据元素。我认为这应该是首选的方法。拥有统一的UI底层模型是良好软件设计的一步。

  2. 在数据模型之间传播更改时,同时实现DisplayRoleEditRole。视图会正常使用其中一种角色(例如EditRole)修改模型,而您可以以编程方式使用另一种角色(例如DisplayRole)修改模型。您需要正确处理角色并处理来自模型的dataChanged信号,并调用具有其他角色的其他模型的setData函数。这可以避免循环。

  3. 对于非QAbstractItemView的控件,请实现两个信号:一个在任何更改时发出,另一个仅在基于键盘/鼠标输入的更改时发出。例如,QAbstractButton暴露的接口就是这样: toggled(bool)信号是前者,clicked()信号是后者。您只需要连接基于输入的信号。

    您的代码必须将编程更改传播到所有互相关联的控件中,因为从代码中更改一个控件不会修改其他控件。由于良好设计的代码应该将UI控件的实现细节封装在其余代码之外,所以这不应该成为问题。因此,您的对话框/窗口类将以未耦合特定属性显示的方式公开其属性。

黑客式的希望它们不会成为俗语的说法

  1. 使用抑制信号发射的标志位 (Bartosz's answer)。

  2. 暂时断开信号与槽的连接 (Bartosz's answer)。

  3. 使用 QObject::blockSignals()


我已经清楚地理解了。第一和第二种解决方案必须是处理多个视图的正确方法。我对这些习语一无所知。但我会尽快学习并使用它们。感谢您的细心回答。 - Cahit Burak Küçüksütcü

2
我可以给出两种可能的解决方案:
  • 添加一个标志,使得可以忽略特定的信号:

 

void MyListWidget::OnSystemSelectionChanged(QObject *sender)
{
    if (sender == this || inhibitSelectionChanged)
        return;

    this->inhibitSelectionChanged = true;
    this->SetSelection(System::Instance()->GetSelectedObjects());
    this->inhibitSelectionChanged = false;
}
  • 断开与信号相连的槽,更改选择后重新连接:

 

void MyListWidget::OnSystemSelectionChanged(QObject *sender)
{
    if (sender == this)
        return;

    this->disconnect(SIGNAL(SelectionChanged()));

    this->SetSelection(System::Instance()->GetSelectedObjects());

    this->connect(
        this, SIGNAL(SelectionChanged()), 
        this, SLOT(OnSystemSelectionChanged(QObject*)));
}

感谢您的建议。第一个建议是发出itemSelectionChanged信号,但我可以使用标志来防止在onSelectionChanged中发出System的SelectionChanged信号。对于第二个建议,我认为了解连接和断开连接的成本非常重要。 - Cahit Burak Küçüksütcü
@CahitBurakKüçüksütcü 在这类应用程序中,成本通常不重要。只有当您发现性能存在问题时,才有必要调查该问题。 - BartoszKP
我理解,但根据状态的不同,它可能会被调用十次,成本可能只有几微秒,因此也可以使用。 - Cahit Burak Küçüksütcü

2

我在QObject::blockSignals()方法中找到了解决方案。在我设置选定项时,它将防止从列表小部件发出信号。

感谢所有答案和解决方案,特别是BartoszKP的。这个解决方案看起来像他的第一个解决方案的官方方式。


不知道这个函数,很有趣,谢谢! - BartoszKP
看起来你的解决方案和 Qt 先前已经记录的一样啊,您就在发布之前了 :) - Cahit Burak Küçüksütcü

1
问题:你尝试走捷径,创建了一个单例,这不是经典的单例情况。
信号和插槽用于通知,每个对象通知感兴趣的对象它所做的事情或反映其新状态。
我建议按照以下方式更改设计:
- 没有单例信号。 - 每个对象都有自己的信号和插槽来处理相关事件(例如选择更改)。 - 应用程序或更高级别的对象(创建小部件/对象的对象)执行信号到插槽的连接。如果这些小部件放在列表中,这非常简单。

可以是MainWindow,谢谢建议,还有其他解决方案吗? - Cahit Burak Küçüksütcü
当然,你可以像BartoszKP的回答建议的那样使用补丁,但从长远来看,你会后悔的。这会留下很多错误的空间。我的建议是采用一个简单的设计,这将使你的代码易于理解。 - egur
我再想了一会儿,发现你的解决方案很痛苦,特别是当我想添加更多小部件时。 - Cahit Burak Küçüksütcü
@CahitBurakKüçüksütcü - 祝你好运! - egur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接