我可以在不修改 QWidget 子类 PIMPL 样式的情况下包装它吗

Can I wrap a QWidget subclass PIMPL style without modifying it

我有一个 class,我想使用 PIMPL 类型的方法隐藏它。这是因为它是一个 UI 形式,它引入了 uic 生成的 header 依赖关系,我不希望代码的其他部分需要这些依赖关系。

至此我将其重命名为PrivateClass,以供说明:

PrivateClass: public QWidget, public Ui_Form
{
 Q_OBJECT:
public: // doesn't need to be public but I'm trying to leave as-is apart from name change
 PrivateClass(QWidget *parent=0) : QWidget(parent)
{
 setupUi(this); // Ui_Form function
}
// etc
 void do_something();
};


MyClass: public QWidget
{
 Q_OBJECT:
public: 
 MyClass(QWidget *parent=0) : QWidget(parent)
 {
   _prvt = new PrivateClass(this); // or pass in parent? 
 }
 ~MyClass()
{
 delete _prvt;
}
 // a bunch of interface functions like this
 void do_something(){ _prvt->do_something();}
private:
PrivateClass *_prvt;
};

我知道 Qt 提供了一个基于宏的 PIMPL 实现,但我想在这里手动执行此操作,它不是很大 class。

所以问题是如何处理 QWidget 方面的事情。要保持 PrivateClass 不变但仍然允许新的包装器 MyClass 插入,它们都必须是 QWidgets。除非我为 QWidget 编写接口,否则对 MyClass 的任何 QWidget 调用都不会传播到 _prvt,这听起来不对。

我暂时重新配置了 PrivateClass,使其不再是 QWidget,并将指向 MyClass 的指针作为构造函数参数,这方面有什么改进吗?

有关如何使用 Qt 的 PIMPL 宏执行此操作的示例,请参阅 this question。由于我们不在下面的代码中使用这些宏,因此必须手动编写一些代码以保持类型安全。

假设你从这个开始 class:

原始界面

#include <QWidget>
#include "ui_widget.h"

class Widget : public QWidget, Ui::Widget {
  int m_something;
public:
  explicit Widget(QWidget * parent = nullptr);
  void do_something();
  int something() const;
  ~Widget();
};

原始实现

#include "widget.h"

Widget::Widget(QWidget * parent) : 
  QWidget{parent},
  m_something{44}
{
  setupUi(this);
}

void Widget::do_something() {
  hide(); // just an example of doing something
}

int Widget::something() const {
  return m_something;
}

Widget::~Widget() {}

Can I wrap a QWidget subclass PIMPL style without modifying it

也许吧。让我们看看它是如何工作的。我们可以单独留下 Widget,将其视为实现细节,并通过 Public 接口 "expose"。我们需要使用中间布局将调整大小和大小约束从界面转发到实现。

包装器小部件界面

#include <QWidget>

class Widget;
class Public : public QWidget {
  Widget * const d_ptr;
  Widget * d();
  const Widget * d() const;
public:
  explicit Public(QWidget * parent = nullptr);
  void do_something();
  int something() const;
};

包装器小部件实现

#include "public.h"
#include "widget.h"

Public::Public(QWidget * parent) :
  QWidget{parent},
  d_ptr{new Widget{this}}
{
  auto const layout = new QVBoxLayout{this};
  layout->setMargin(0);
}

Widget * Public::d() {  return d_ptr.data(); }
const Widget * Public::d() const { return d_ptr.data(); }

void Public::do_something() {
  d()->do_something();
}

int Public::something() const {
  return d()->something();
}

这有一些问题:

  1. 您正在为额外的小部件和布局实例付出代价。

  2. 中间布局可以巧妙地破坏封闭的 封闭小部件的行为。 Qt 布局并不完美;由于它们的设计,它们遭受数值近似行为和封装实体行为的不完美转发。添加额外的层强调了这一缺陷。

相反,你真的很想修改原来的class。只是 PIMPL 并完成它要简单得多。如果您做好了准备,这可能是一个非常机械的代码转换,会产生一个合理的差异。

所以,现在您需要 PIMPL 版。将所有方法从 Widget 推送到 WidgetPrivate,并且只将转发器方法添加到 public 接口将是最简单的。

接口丢失所有个私有成员,并添加了d_ptrd()

PIMPL-ed 界面

#include <QWidget>

class WidgetPrivate;
class Widget : public QWidget {
  QScopedPointer<WidgetPrivate> const d_ptr;
  WidgetPrivate* d();
  const WidgetPrivate* d() const;
public:
  explicit Widget(QWidget * parent = nullptr);
  void do_something();
  int something() const;
  ~Widget();
};

PIMPL 通过 q 指针访问 QWidget

PIMPL 版实施

#include "widget.h"
#include "ui_widget.h"

class WidgetPrivate : public Ui::Widget {
public:
  Widget * const q_ptr;
  inline Widget * q() { return q_ptr; }
  inline const Widget * q() const { return q_ptr; }
  int m_something;

  WidgetPrivate(Widget * q)
  void init();
  void do_something();
  int something() const;
};

///vvv This section is from "original.cpp" with simple changes:

WidgetPrivate(Widget * q) :
  q_ptr{q},
  m_something{44}
{}

/// Can only be called after the QWidget is fully constructed.
void WidgetPrivate::init() {
  auto const q = q(); // Widget * - we can use a local `q` 
                      // to save on typing parentheses
  setupUi(q);
}

void WidgetPrivate::do_something() {
  q()->hide(); // we can use q() directly
}

int WidgetPrivate::something() const {
  return m_something;
}

///^^^

WidgetPrivate * Widget::d() {  return d_ptr.data(); }
const WidgetPrivate* Widget::d() const { return d_ptr.data(); }

Widget::Widget(QWidget * parent) :
  QWidget{parent},
  d_ptr{new WidgetPrivate{this}}
{
  d()->init();
}

void Widget::do_something() {
  d()->do_something();
}

int Widget::something() const {
  return d()->something();
}

// If the compiler-generated destructor doesn't work then most likely
// the design is broken.
Widget::~Widget() {}

d()q() 方法需要 return 从常量方法访问时常量正确的 PIMPL 和 Q。

将这个相当机械的更改签入版本控制系统后,您可以选择从 PIMPL 中删除琐碎的方法并将它们移到界面中:

重新设计的 PIMPL 实现

#include "widget.h"
#include "ui_widget.h"

class WidgetPrivate : public Ui::Widget {
public:
  Widget * const q_ptr;
  inline Widget * q() { return q_ptr; }
  inline const Widget * q() const { return q_ptr; }
  int m_something;

  WidgetPrivate(Widget * q) : q_ptr{q} {}
  void init();
};

WidgetPrivate * Widget::d() {  return d_ptr.data(); }
const WidgetPrivate* Widget::d() const { return d_ptr.data(); }

WidgetPrivate(Widget * q) :
  q_ptr{q},
  m_something{44}
{}

/// Can only be called after the QWidget is fully constructed.
void WidgetPrivate::init() {
  setupUi(q());
  // let's pretend this was a very long method that would have
  // much indirection via `d->` if it was moved to `Widget`'s constructor
}

void Widget::do_something() {
  hide();
}

int Widget::something() const {
  return d()->m_something;
}

Widget::Widget(QWidget * parent) :
  QWidget{parent},
  d_ptr{new WidgetPrivate{this}}
{
  d()->init();
}

Widget::~Widget() {}