我可以不修改QWidget子类的PIMPL样式包装它吗?

Can I wrap a QWidget subclass PIMPL style without modifying it

本文关键字:包装 样式 PIMPL 修改 QWidget 子类 我可以      更新时间:2023-10-16

我有一个类,我想用PIMPL类型方法隐藏它。这是因为它是一个UI表单,引入了我不希望其他代码部分需要的UI生成的头依赖项。

到目前为止,我已经将其重命名为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实现,但我想在这里手动做到这一点,这不是一个巨大的类。

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

我暂时重新配置了PrivateClass,使其不再是一个QWidget,并将指向MyClass的指针作为构造函数参数,对此有任何改进吗?

关于如何使用Qt的PIMPL宏的示例,请参阅此问题。由于在下面的代码中没有使用这些宏,因此必须手工编写一些代码来维护类型安全。

假设您从这个类开始:

原始界面
#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() {}

我可以包装一个QWidget子类PIMPL样式而不修改它吗

。让我们看看它是如何工作的。我们可以不考虑Widget,将其作为实现细节,并通过Public接口"公开"它。我们需要使用中间布局将调整大小和大小约束从接口转发到实现。

包装器小部件接口

#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布局并不完美;由于它们的设计,它们受到数值近似行为和封装实体行为不完全转发的影响。

相反,您真正想要修改的是原始类。PIMPL就简单多了。如果你准备好了,它可以是一个非常机械的代码转换,将产生一个合理的差异。

那么,现在你想要它pplp -ed。将Widget中的所有方法都推到WidgetPrivate中,并且只在公共接口中添加转发器方法是最简单的。

接口丢失所有私有成员,增加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-ed实现
#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()方法来返回const正确的PIMPL和Q。

在将这个相当机械的更改签入到版本控制系统之后,您可以选择从PIMPL中删除一些不重要的方法,并将它们移到接口中:

重新设计的PIMPL-ed实现

#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() {}