六、创建小部件
T 术语小部件是组成应用程序的各种视觉元素的统称:按钮、标题栏、文本框、复选框等等。关于使用窗口小部件创建用户界面,有两种观点:要么坚持使用标准的窗口小部件,要么冒险创建自己的窗口小部件。Qt 两者都支持。
除非你有深奥的需求,否则你应该尽可能地坚持使用既定的小部件。当您使用 Qt 时,这让您的生活变得非常简单,因为标准的小部件在大多数平台上都是原生的。但是,如果你想在野外行走,你可以利用 Qt 出色的造型能力,继承小部件并覆盖它们的绘画;或者简单地创建自己的小部件。在某些情况下,您需要这样做,因为您的应用程序处理无法以其他方式显示的数据。本章向您展示了如何调整和创建小部件来满足您自己的需求。
编写小工具
你每次都以同样的方式组合相同的部件吗?复合小部件会有所帮助。一个复合小部件是通过组合已经存在的小部件并为它们提供一组良好的属性、信号和插槽来构建的。
例如,键盘很难管理。图 6-1 显示了一个由一串QPushButton
和一个QLineEdit
组成的小键盘。设置它包括创建一个网格布局,将小部件放入布局中,然后进行连接以使事情正常工作。
图 6-1。 由一个 QLineEdit
和一组 QPushButton
小工具组成的小键盘
让我们看看小部件集合的哪些部分是“有趣的”,哪些部分是“不有趣的”(“不有趣”类别中的所有内容都是不必要的复杂)。这种复杂性可以通过创建复合小部件来隐藏。
应用程序的其余部分需要知道QLineEdit
的文本;其他一切只是混淆了你的应用程序的源代码。清单 6-1 展示了NumericKeypad
类的类声明。如果您关注信号和公共部分,您会发现文本是所有可用的内容。私有部分涉及小部件的内部:文本、行编辑和一个用于捕捉按钮输入的槽。
清单 6-1。 复合小部件的类声明NumericKeypad
class NumericKeypad : public QWidget
{
Q_OBJECT
public:
NumericKeypad( QWidget *parent = 0 );
const QString& text() const;
public slots:
void setText( const QString &text );
signals:
void textChanged( const QString &text );
private slots:
void buttonClicked( const QString &text );
private:
QLineEdit *m_lineEdit;
QString m_text
};
在了解如何管理文本之前,您应该了解小部件是如何构造的。您可以从类声明中看出这个小部件是基于一个QWidget
的。在构造器中,一个布局被应用到QWidget
(this
);然后将QLineEdit
和QPushButton
小部件放到布局中。源代码如清单 6-2 所示。
清单 6-2。 在构造器中创建和布局按钮
NumericKeypad::NumericKeypad( QWidget *parent )
{
QGridLayout *layout = new QGridLayout( this );
m_lineEdit = new QLineEdit
m_lineEdit->setAlignment( Qt::AlignRight );
QPushButton *button0 = new QPushButton( tr("0") );
QPushButton *button1 = new QPushButton( tr("1") );
...
QPushButton *buttonDot = new QPushButton( tr(".") );
QPushButton *buttonClear = new QPushButton( tr("C") );
layout->addWidget( m_lineEdit, 0, 0, 1, 3 );
layout->addWidget( button1, 1, 0 );
layout->addWidget( button2, 1, 1 );
...
layout->addWidget( buttonDot, 4, 1 );
layout->addWidget( buttonClear, 4, 2 );
...
}
您可能会发现前一个示例中遗漏的构造器部分更有趣。每个QPushButton
对象,除了 C 按钮,都使用QSignalMapper
的setMapping(QObject *, const QString&)
方法映射到一个QString
。设置好所有映射后,来自按钮的clicked()
信号都连接到信号映射器的map()
插槽。调用map
时,信号映射器会查看信号发送器,通过mapped(const QString&)
信号发出映射后的字符串。该信号依次连接到this
的buttonClicked(const QString&)
插槽。你可以在清单 6-3 中看到这是如何设置的。
清单还显示 C 按钮的clicked
信号被映射到QLineEdit
的clear
槽,QLineEdit
的textChanged
信号被连接到小键盘小部件的setText
方法。这意味着单击 C 按钮会清除文本;对QLineEdit
的任何更改——无论是通过用户交互还是按下 C 按钮——都会更新NumericKeypad
对象的文本。
清单 6-3。 在构造器中设置信号映射
NumericKeypad::NumericKeypad( QWidget *parent )
{
...
layout->addWidget( buttonDot, 4, 1 );
layout->addWidget( buttonClear, 4, 2 );
QSignalMapper *mapper = new QSignalMapper( this );
mapper->setMapping( button0, "0" );
mapper->setMapping( button1, "1" );
...
mapper->setMapping( button9, "9" );
mapper->setMapping( buttonDot, "." );
connect( button0, SIGNAL(clicked()), mapper, SLOT(map()) );
connect( button1, SIGNAL(clicked()), mapper, SLOT(map()) );
...
connect( button9, SIGNAL(clicked()), mapper, SLOT(map()) );
connect( buttonDot, SIGNAL(clicked()), mapper, SLOT(map()) );
connect( mapper, SIGNAL(mapped(QString)), this, SLOT(buttonClicked(QString)) );
connect( buttonClear, SIGNAL(clicked()), m_lineEdit, SLOT(clear()) );
connect( m_lineEdit, SIGNAL(textChanged(QString)), this, SLOT(setText(QString)) );
}
处理文本变化的插槽如清单 6-4 中的所示。buttonClicked
槽只是将新文本附加到当前文本的末尾,当前文本保存在QString
变量m_text
中。文本保存在一个单独的字符串中,而不仅仅是保存在QLineEdit
中,因为用户可以通过在编辑器中键入来直接更改文本。如果做了这样的改变,你无法判断setText
的电话是否相关,因为你无法比较当前文本和新文本。这可能导致textChanged
方法在没有实际变化发生的情况下被发出。
提示您可以通过将文本编辑器的 enabled 属性设置为false
来解决这个问题,但是这会导致编辑器看起来不一样。
清单 6-4。 处理文字的变化
void NumericKeypad::buttonClicked( const QString &newText )
{
setText( m_text + newText );
}
void NumericKeypad::setText( const QString &newText )
{
if( newText == m_text )
return;
m_text = newText;
m_lineEdit->setText( m_text );
emit textChanged( m_text );
}
setText
槽从检查是否发生了实际变化开始。如果是这样,内部文本和QLineEdit
文本都会被更新。然后发出带有新文本的textChanged
信号。
任何对QLineEdit
的文本感兴趣的外部小部件都可以连接到textChanged
信号或者通过调用text
方法来询问。如清单 6-5 所示,这个方法很简单——它返回m_text
。
清单 6-5。 返回当前文本
const QString& NumericKeypad::text() const
{
return m_text;
}
使用复合小部件和使用普通小部件一样简单。在清单 6-6 中,你可以看到如何使用NumericKeypad
小部件。键盘放在一个标签上只是为了测试textChanged
信号。标签的setText
插槽连接到键盘的textChanged
信号。图 6-2 显示了实际应用。QLineEdit
的文字始终通过QLabel
反映出来。
清单 6-6。 使用 NumericKeypad
控件
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QWidget widget;
QVBoxLayout *layout = new QVBoxLayout( &widget );
NumericKeypad pad;
layout->addWidget( &pad );
QLabel *label = new QLabel;
layout->addWidget( label );
QObject::connect( &pad, SIGNAL(textChanged(const QString&)),
label, SLOT(setText(const QString&)) );
widget.show();
return app.exec();
}
图 6-2。 复合小部件直播
编写小部件有很多好处。使用带有main
功能的NumericKeypad
小部件比在那里配置所有按钮和QLineEdit
小部件要容易得多。此外,信号和插槽创建了一个很好的界面来连接键盘和其他小部件。
后退一步,看看小部件本身——您会发现组件远比如何设置解决方案的知识更具可重用性。这使得它更有可能在应用程序中的更多地方使用(或在更多应用程序中使用)。一旦您使用它两次,您将节省开发时间和精力,因为您只需要设置一次信号映射器。您也知道它是可行的,因为您已经验证过一次了——省去了您定位 bug 的问题。
改变和增强小工具
定制小部件的另一种方法是改变或增强它们的行为。比如一个QLabel
可以做出一个很棒的数字时钟小部件;缺少的只是用当前时间更新文本的部分。由此产生的小部件可以在图 6-3 的中看到。
图 6-3。 一个充当时钟的标签
通过使用已经存在的小部件作为新小部件的起点,您可以避免开发绘制、大小提示等所需的所有逻辑。相反,您可以专注于用您需要的功能来增强小部件。让我们看看这是如何做到的。
首先,必须有一种方法以均匀的间隔检查时间,例如每秒一次。每次检查时,文本都必须更新为当前时间。要查看每秒钟的时间,可以使用一个QTimer
。可以设置一个定时器对象,在给定的时间间隔发出timeout
信号。通过将此信号连接到时钟标签的一个插槽,您可以检查时间并每秒相应地更新文本。
清单 6-7 显示了ClockLabel
小部件的类声明。它有一个槽,updateTime
,和一个构造器。这(和继承QLabel
)就是实现这个定制行为所需要的全部。
清单 6-7。ClockLabel
类声明
*`class ClockLabel : public QLabel
{
Q_OBJECT
public:
ClockLabel( QWidget *parent = 0 );
private slots:
void updateTime();
};`
你可以在清单 6-8 中看到ClockLabel
小部件的实现。从底部开始,updateTime()
槽非常简单——它所做的只是将文本设置为当前时间。QTime::toString()
方法根据格式化字符串将时间转换为字符串,其中hh
表示当前小时,mm
表示分钟。
在构造器中创建了一个QTimer
对象。间隔(发出timeout
信号的频率)设置为 1000 毫秒(1 秒)。
提示将毫秒数除以 1000,得到相等的秒数。1000 毫秒相当于 1 秒。
当定时器的时间间隔设定后,定时器的timeout()
信号在定时器启动前连接到this
的updateTime
信号。QTimer
物体必须在开始周期性发射timeout
信号之前启动。使用stop()
方法关闭信号发射。这意味着您可以设置一个计时器,然后根据应用程序的当前状态打开和关闭它。
注意 QTimer
对象对于用户界面等来说已经足够好了,但是如果你正在开发一个需要精确计时的应用程序,你必须使用另一种解决方案。间隔的准确性取决于应用程序运行的平台。
在构造器完成之前,对updateTime
进行显式调用,这确保了文本立即更新。否则,在文本更新之前需要一秒钟,用户将能够在短时间内看到未初始化的小部件。
清单 6-8。ClockLabel
实现
ClockLabel::ClockLabel( QWidget *parent ) : QLabel( parent )
{
QTimer *timer = new QTimer( this );
timer->setInterval( 1000 );
connect( timer, SIGNAL(timeout()), this, SLOT(updateTime()) );
timer->start();
updateTime();
}
void ClockLabel::updateTime()
{
setText( QTime::currentTime().toString( "hh:mm" ) );
}
有时你可能想增强一个现有的部件;例如,您可能希望一个插槽接受另一种类型的参数,或者在缺少插槽的地方。您可以继承基本小部件,添加插槽,然后使用结果类而不是原始类。
抓捕事件
小部件通过提供对触发信号和提供交互的实际用户生成事件的访问,为处理用户动作提供了催化剂。事件是用户给计算机的原始输入。通过对这些事件作出反应,用户界面可以与用户交互并提供预期的功能。
事件由事件处理程序处理,事件处理程序是虚拟的受保护方法,当小部件类需要对给定事件做出反应时,它们会覆盖这些方法。每个事件都伴随着一个事件对象。所有事件类的基类是QEvent
,它使接收者能够接受或忽略使用相同名称的方法的事件。被忽略的事件可以通过 Qt 传播到父部件。
图 6-4 显示了QApplication
接收到的触发事件的用户动作。这些事件导致应用程序调用受影响的小部件,小部件对事件做出反应,并在必要时发出信号。
图 6-4。 用户动作在到达小部件之前通过 QApplication
对象,并触发驱动应用程序的信号
监听用户
为了更好地理解事件处理是如何工作的,您可以创建一个小部件,它发出一个带有字符串的信号,告诉您刚刚收到了哪个事件。widget 类叫做EventWidget
,信号叫做gotEvent(const QString &)
。通过将这个信号与一个QTextEdit
挂钩,您可以获得一个事件日志,您可以使用它来研究这些事件。
首先快速浏览一下清单 6-9 。EventWidget
有一系列的事件处理程序,下面的列表描述了每个事件处理程序的职责。这些事件处理方法是一些最常见的方法,但还有更多。在列表的每一行中,我保留了事件对象类型和事件名称,这样您就可以看到哪些事件是相关的。例如,所有焦点事件都将一个QFocusEvent
指针作为参数。
closeEvent( QCloseEvent* )
:小部件即将关闭。(你在第四章中看到了这是如何使用的。)contextMenuEvent( QContextMenuEvent* )
:请求上下文菜单。enterEvent( QEvent* )
:鼠标指针已经进入小部件。focusInEvent( QFocusEvent* )
:小工具获得焦点。focusOutEvent( QFocusEvent* )
:焦点离开小工具。hideEvent( QHideEvent* )
:小工具即将被隐藏。keyPressEvent( QKeyEvent* )
:键盘按键被按下。keyReleaseEvent( QKeyEvent* )
:释放了一个键盘键。leaveEvent( QEvent* )
:鼠标指针离开了小工具。mouseDoubleClickEvent( QMouseEvent* )
:鼠标按钮被双击。mouseMoveEvent( QMouseEvent* )
:鼠标在小工具上移动。mousePressEvent( QMouseEvent* )
:鼠标按钮被按下。mouseReleaseEvent( QMouseEvent* )
:鼠标按钮已被释放。- 小工具需要重新绘制。
resizeEvent( QResizeEvent* )
:小工具已调整大小。showEvent( QShowEvent* )
:即将显示小工具。wheelEvent( QWheelEvent* )
:鼠标滚动视图被移动。
在前面的列表中,您可以看到相关事件共享事件对象类型。例如,所有鼠标事件——比如按下、释放、移动和双击——都需要一个QMouseEvent
。
只有一个QEvent
的事件可以被认为是简单的通知。在QEvent
对象中没有携带额外的信息,所以只需要知道事件发生了。因为QEvent
是所有事件类的基类,所以共享QEvent
作为事件对象类型的事件处理程序不像鼠标事件那样相关。
一些事件处理程序被排除在列表和EventWidget
类之外。尽管缺少的处理程序并没有降低相关性,但是它们与类中使用的处理程序并没有显著的不同。
清单 6-9。EventWidget
实现了大多数事件处理程序,并为每个事件发出 gotEvent
信号。
class EventWidget : public QWidget
{
Q_OBJECT
public:
EventWidget( QWidget *parent = 0 );
signals:
void gotEvent( const QString& );
protected:
void closeEvent( QCloseEvent * event );
void contextMenuEvent( QContextMenuEvent * event );
void enterEvent( QEvent * event );
void focusInEvent( QFocusEvent * event );
void focusOutEvent( QFocusEvent * event );
void hideEvent( QHideEvent * event );
void keyPressEvent( QKeyEvent * event );
void keyReleaseEvent( QKeyEvent * event );
void leaveEvent( QEvent * event );
void mouseDoubleClickEvent( QMouseEvent * event );
void mouseMoveEvent( QMouseEvent * event );
void mousePressEvent( QMouseEvent * event );
void mouseReleaseEvent( QMouseEvent * event );
void paintEvent( QPaintEvent * event );
void resizeEvent( QResizeEvent * event );
void showEvent( QShowEvent * event );
void wheelEvent( QWheelEvent * event );
};
在继续查看事件处理程序之前,先看一下main
函数,它显示了带有日志的小部件。源代码如清单 6-10 所示。日志显示在一个QTextEdit
小部件中,gotEvent
信号连接到日志的append(const QString&)
槽。这是显示小部件和运行应用程序之前需要做的所有准备工作。
清单 6-10。 创建一个日志小工具和一个 EventWidget
并使用它们
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QTextEdit log;
EventWidget widget;
QObject::connect( &widget, SIGNAL(gotEvent(const QString&)),
&log, SLOT(append(const QString&)) );
log.show();
widget.show();
return app.exec();
}
当应用程序运行时,日志窗口显示在包含事件小部件的窗口旁边。示例日志如图 6-5 中的所示。列出所有事件,并显示某些事件的选定参数。例如,QKeyEvent
事件显示文本,而QMouseEvent
事件显示指针位置。
图 6-5。 来自 EventWidget
清单 6-11 提供了一个closeEvent
处理程序的例子。enterEvent
、leaveEvent
、showEvent
、hideEvent
和paintEvent
处理程序都只是记录事件的名称。show、hide 和 paint 事件有自己的事件对象类型。QShowEvent
和QHideEvent
类不向QEvent
类添加任何东西。QPaintEvent
确实增加了很多信息(在本章的后面你会更仔细地观察这个事件)。
清单 6-11。 一种简单的事件处理方法
void EventWidget::closeEvent( QCloseEvent * event )
{
emit gotEvent( tr("closeEvent") );
}
处理键盘事件
处理键盘活动的事件是keyPressEvent
和keyReleaseEvent
。它们看起来都很相似,所以在清单 6-12 中只显示了keyPressEvent
。因为大多数现代环境支持自动重复键,所以在看到 keyReleaseEvent
之前,您可能会得到几个keyPressEvent
。您通常不能指望看到keyReleaseEvent
——用户可能会在释放按键之前在部件之间移动焦点(使用鼠标)。
如果您需要确保您的小部件获得所有键盘事件的*,请使用grabKeyboard
和releaseKeyboard
方法。当一个小部件抓取键盘时,所有的按键事件都会发送给它,不管哪个小部件当前拥有焦点。*
清单中的事件处理程序显示了修饰键和被按下的键的文本。修改器存储为一个位掩码,几个修改器可以同时激活。
清单 6-12。 一个键盘事件的处理方法
void EventWidget::keyPressEvent( QKeyEvent * event ) { emit gotEvent( QString("keyPressEvent( text:%1, modifiers:%2 )") .arg( event->text() ) .arg( event->modifiers()==0?tr("NoModifier"):( (event->modifiers() & Qt::ShiftModifier ==0 ? tr(""): tr("ShiftModifier "))+ (event->modifiers() & Qt::ControlModifier ==0 ? tr(""): tr("ControlModifier "))+ (event->modifiers() & Qt::AltModifier ==0 ? tr(""): tr("AltModifier "))+ (event->modifiers() & Qt::MetaModifier ==0 ? tr(""): tr("MetaModifier "))+ (event->modifiers() & Qt::KeypadModifier ==0 ? tr(""): tr("KeypadModifier "))+ (event->modifiers()&Qt::GroupSwitchModifier ==0 ? tr(""): tr("GroupSwitchModifier")) ) ) ); }
处理鼠标事件
当用户试图调出上下文菜单(右键单击某个东西时出现的菜单——通常提供剪切、复制和粘贴等操作)时,就会触发上下文菜单事件。鼠标和键盘都可以触发该事件。事件对象包含请求的来源(reason
)和事件发生时鼠标指针的坐标。处理程序如清单 6-13 中的所示。如果上下文菜单事件被忽略,它将被重新解释并作为鼠标事件发送(如果可能)。
所有带有鼠标位置的事件对象都有pos()
和globalPos()
方法。pos
方法是小部件本地坐标中的位置,这有利于更新小部件本身。如果您想在事件发生的位置创建一个新的小部件,您需要使用全局坐标。位置由x
和y
坐标组成,可以通过x
、y
、globalX
和globalY
方法直接从事件对象中获取。
清单 6-13。 已请求上下文菜单。
void EventWidget::contextMenuEvent( QContextMenuEvent * event )
{
emit gotEvent( QString("contextMenuEvent( x:%1, y:%2, reason:%3 )")
.arg(event->x())
.arg(event->y())
.arg(event->reason()==QContextMenuEvent::Other ? "Other" :
(event->reason()==QContextMenuEvent::Keyboard ? "Keyboard" :
"Mouse")) );
}
上下文菜单事件携带鼠标位置,就像QMouseEvent
一样。鼠标事件有mousePressEvent
、mouseReleaseEvent
、mouseMoveEvent
和mouseDoubleClickEvent
。你可以在清单 6-14 中看到后者。处理器显示button
以及x
和y
坐标。
在处理鼠标事件时,重要的是要理解只有当鼠标按钮被按下时,移动事件才会被发送。如果您需要随时获取移动事件,您必须使用mouseTracking
属性启用鼠标跟踪。
如果你想得到所有的鼠标事件,你可以像使用键盘一样使用鼠标。为此使用方法grabMouse()
和releaseMouse()
。只是要小心,因为当鼠标被抓取时发生的错误会阻止所有应用程序的鼠标交互。规则是只在必要时抓取,尽快释放,并且永远不要忘记释放鼠标。
清单 6-14。 一个鼠标事件的处理方法
void EventWidget::mouseDoubleClickEvent( QMouseEvent * event )
{
emit gotEvent( QString("mouseDoubleClickEvent( x:%1, y:%2, button:%3 )")
.arg( event->x() )
.arg( event->y() )
.arg( event->button()==Qt::LeftButton? "LeftButton":
event->button()==Qt::RightButton?"RightButton":
event->button()==Qt::MidButton? "MidButton":
event->button()==Qt::XButton1? "XButton1":
"XButton2" ) );
}
使用鼠标滚轮
鼠标滚轮通常被认为是鼠标的一部分,但是事件有一个单独的事件对象。该对象包含事件发生时鼠标指针的位置,以及滚轮的方向和滚动的大小(delta
)。事件处理程序如清单 6-15 中的所示。
鼠标滚轮事件首先被发送到鼠标指针下的小部件。如果它没有在那里被处理,它将被传递给具有焦点的小部件。
清单 6-15。 轮子与鼠标的其他部分是分开的。
void EventWidget::wheelEvent( QWheelEvent * event )
{
emit gotEvent( QString("wheelEvent( x:%1, y:%2, delta:%3, orientation:%4 )")
.arg( event->x() )
.arg( event->y() )
.arg( event->delta() ).arg( event->orientation()==Qt::Horizontal?
"Horizontal":"Vertical" ) );
}
在EventWidget
类中实现了更多的事件处理程序。通过在小部件上尝试不同的东西,然后研究日志,您可以了解很多关于小部件的信息。
过滤事件
创建事件过滤器比继承小部件类和覆盖事件处理类更容易。一个事件过滤器是一个继承QObject
的类,它实现了eventFilter(QObject*, QEvent*)
方法。该方法使得在事件到达目的地之前拦截它们成为可能。然后可以过滤事件(允许通过或停止)。
事件过滤器可用于实现许多特殊功能,如鼠标手势和识别按键序列。它们可以用来增强小部件或改变小部件的行为,而不必对小部件进行子类化。
让我们尝试一个事件过滤器,从事件队列中删除任何数字键按压。该类的声明和实现如清单 6-16 所示。有趣的部分是eventFilter
方法,它有两个参数:指向目的地QObject
( dest
)的指针和指向QEvent
对象(event
)的指针。通过使用type
检查事件是否是按键事件,您知道event
指针可以被转换为QKeyEvent
指针。QKeyEvent
类有一个 text 方法,您可以用它来确定按下的键是否是一个数字。
如果按键来自数字键,则返回true
,表示过滤器处理了该事件。这将阻止事件到达目标对象。对于所有其他事件,将返回基类实现的值,这将导致要么由基类筛选器处理事件,要么让它通过最终的目标对象。
清单 6-16。 事件过滤类 KeyboardFilter
停止按键为数字键。
class KeyboardFilter : public QObject
{
public:
KeyboardFilter( QObject *parent = 0 ) : QObject( parent ) {}
protected:
bool eventFilter( QObject *dist, QEvent *event )
{
if( event->type() == QEvent::KeyPress )
{
QKeyEvent *keyEvent = static_cast<QKeyEvent*>( event );
static QString digits = QString("1234567890");
if( digits.indexOf( keyEvent->text() ) != −1 )
return true;
}
return QObject::eventFilter(dist, event);
}
};
为了测试事件过滤器,你可以把它安装在一个QLineEdit
(它的源代码显示在清单 6-17 )上。像任何其他对象一样创建QLineEdit
和KeyboardFilter
对象。然后在编辑器显示之前,使用installEventFilter(QObject*)
在编辑行上安装过滤器。
清单 6-17。 要使用事件过滤器,必须将其安装在 widget 上。然后,该小部件的事件通过过滤器传递。
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QLineEdit lineEdit;
KeyboardFilter filter;
lineEdit.installEventFilter( &filter );
lineEdit.show();
return app.exec();
}
尝试使用行编辑。按键被过滤,但是仍然可以使用剪贴板将数字强制输入到编辑器中。您在实现和应用事件过滤器时必须小心——可能会有难以预见的副作用。
如果您在设计过滤器时非常小心,您可以通过过滤、响应和重定向事件来增强应用程序,从而使用户的交互更加容易。例如,在绘图区域捕捉键盘事件,将它们重定向到文本编辑器,并移动焦点。这使得用户无需在输入文本之前点击文本编辑器,使得应用程序更加用户友好。
从头开始创建定制小工具
当其他方法都不起作用时,或者如果您选择遵循一种不同的方法,您可能会陷入这样的境地:您必须创建自己的小部件。创建一个定制的小部件包括实现一个信号和插槽的接口,以及一组适用的事件处理程序。
为了向你展示这是如何做到的,我将通过CircleBar
小部件来指导你(见图 6-6 )。图中所示的应用程序在水平滑块上有一个CircleBar
小部件。移动滑块会更改圆栏的值,当鼠标悬停在圆栏小工具上时旋转鼠标滚轮也是如此。
CircleBar
控件的功能是通过改变实心圆的大小来显示 0 到 100 之间的值。一个完整的圆圈表示 100,而中间的圆点表示 0。用户可以使用鼠标滚轮更改显示的值。
图 6-6。CircleBar
小部件和一个水平滑块
*main
函数,如清单 6-18 所示,设置滑块和圆形条。代码首先为保存滑块和圆形条的QVBoxLayout
创建一个基本小部件。滑动条和圆形条相互连接,因此来自其中一个的valueChanged
信号导致对另一个的setValue
调用。然后在应用程序启动前显示基本小部件。
清单 6-18。 设置 CircleBar
和滑块
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QWidget base;
QVBoxLayout *layout = new QVBoxLayout( base );
CircleBar *bar = new CircleBar;
QSlider *slider = new QSlider( Qt::Horizontal );
layout->addWidget( bar );
layout->addWidget( slider );
QObject::connect( slider, SIGNAL(valueChanged(int)), bar, SLOT(setValue(int)) );
QObject::connect( bar, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int)) );
base.show();
return app.exec();
}
从main
函数中你可以看到CircleBar
小部件需要一个setValue(int)
插槽和一个valueChanged(int)
信号。为了使接口完整,还需要有一个value
方法来读取值。
因为小部件是由代码绘制的,所以paintEvent
需要重新实现。您还需要重新实现wheelEvent
,因为您想要监听鼠标滚轮的活动。我选择添加一个heightForWidth
函数,它将用于保持窗口小部件的方形,以及一个sizeHint
方法,它给了窗口小部件一个很好的起始尺寸。
清单 6-19 中的类声明总结了所有这些。
清单 6-19。CircleBar
控件类的类声明
class CircleBar : public QWidget
{
Q_OBJECT
public:
CircleBar( int value = 0, QWidget *parent = 0 );
int value() const;
int heightForWidth( int ) const;
QSize sizeHint() const;
public slots:
void setValue( int );
signals:
void valueChanged( int );
protected:
void paintEvent( QPaintEvent* );
void wheelEvent( QWheelEvent* );
private:
int m_value;
};
清单 6-20 中所示的CircleBar
类的构造器首先初始化保存在m_value
成员中的内部值。它还创建了一个新的大小策略,该策略在两个方向上都是首选的,并告诉布局管理系统监听heightForWidth
方法。
清单 6-20。CircleBar
构件
CircleBar::CircleBar( int value, QWidget *parent ) : QWidget( parent )
{
m_value = value;
QSizePolicy policy( QSizePolicy::Preferred, QSizePolicy::Preferred );
policy.setHeightForWidth( true );
setSizePolicy( policy );
}
大小策略伴随着返回首选小部件大小的heightForWidth(int)
方法和sizeHint
方法。这些方法的实现如清单 6-21 所示。heightForWidth
方法将宽度作为参数,并将想要的高度返回给布局管理器。CircleBar
类中使用的实现将以高度的形式返回给定的宽度,从而得到一个正方形小部件。
清单 6-21。 尺寸处理方法
int CircleBar::heightForWidth( int width ) const
{
return width;
}
QSize CircleBar::sizeHint() const
{
return QSize( 100, 100 );
}
处理值value()
和setValue
的方法如列表 6-22 所示。value
方法很简单——它简单地返回m_value
。在检查是否发生变化之前,setValue
方法将值限制在 0-100 的范围内。如果是,则在调用update
并发出valueChanged
信号之前更新m_value
。
通过调用update()
,重画事件被触发,这导致对paintEvent
的调用。请记住,您不能在paintEvent
方法之外绘制小部件。相反,调用update
然后从paintEvent
方法处理绘画。
清单 6-22。 移交 CircleBar
小部件的值
int CircleBar::value() const
{
return m_value;
}
void CircleBar::setValue( int value )
{
if( value < 0 )
value = 0;
if( value > 100 )
value = 100;
if( m_value == value )
return;
m_value = value;
update();
emit valueChanged( m_value );
}
在清单 6-23 中,你可以看到paintEvent
方法的实现。在看代码之前,您应该知道autoFillBackground
属性是如何工作的。只要它被设置为true
(默认),在进入paintEvent
方法之前,小部件的背景就用适当的颜色填充。这意味着我们不必担心在绘制之前清除小部件的区域。
在paintEvent
方法中计算radius
和factor
辅助变量。然后创建一个QPainter
对象来绘制小部件。先将钢笔设置为黑色,画外圆;然后笔刷设置为黑色,画内圆。钢笔用来画圆的轮廓;笔刷是用来填充的。默认情况下,两者都设置为不绘制任何内容,因此仅在绘制外圆之前设置笔会给出一个圆轮廓。
清单 6-23。 画外圆和内圆
void CircleBar::paintEvent( QPaintEvent *event )
{
int radius = width()/2;
double factor = m_value/100.0;
QPainter p( this );
p.setPen( Qt::black );
p.drawEllipse( 0, 0, width()-1, width()-1 );
p.setBrush( Qt::black );
p.drawEllipse( (int)(radius*(1.0-factor)),
(int)(radius*(1.0-factor)),
(int)((width()-1)*factor)+1,
(int)((width()-1)*factor)+1 );
}
CircleBar
小部件的最后一部分是wheelEvent
方法(见清单 6-24 )。首先,在使用setValue
更新值之前,事件被接受。
QWheelEvent
对象的delta
值表示滚动移动了多少度。大多数鼠标一次滚动 15 度,因此滚轮上的每次“点击”对应 120 度的增量。我选择将 delta 值除以 20,然后用它来改变值。我凭感觉选择了值 20——该条的大小调整得足够快,同时仍能提供足够的精度。
清单 6-24。 根据滚轮移动更新数值
void CircleBar::wheelEvent( QWheelEvent *event )
{
event->accept();
setValue( value() + event->delta()/20 );
}
定制小部件由两部分组成:对应用程序其余部分可见的属性(value
和setValue
)和事件处理程序(paintEvent
和wheelEvent
)。几乎所有的定制窗口小部件都重新实现了paintEvent
方法,而其余要重新实现的事件处理程序是通过确定哪些是实现所需功能所需要的来挑选的。
您的部件和设计者
在您创建了自己的小部件之后,您可能希望将它与 Designer 集成在一起。这样做的好处是,您不会因为使用定制的小部件而被迫离开设计器工作流。另一个好处是,如果你为其他人开发小部件,你可以让他们使用你的小部件和标准的 Qt 小部件。
将小部件与设计器集成有两种方法:一种简单的方法和一种复杂的方法。比较这两种方法,简单的方法在使用 Designer 时需要做更多的工作,而复杂的方法可以与 Designer 无缝集成。让我们从简单的方法开始。
晋级
您可以使用您在本章前面创建的ClockWidget
来测试将您的小部件与 Designer 集成的推广方式。因为它是基于一个QLabel
,所以在你正在设计的表单上画一个QLabel
。现在调出标签的上下文菜单,并选择“升级到自定义小部件”菜单项,这将调出如图 6-7 所示的对话框。该图形有一个类名—头文件名称由设计器自动猜测。
图 6-7。 将一个 QLabel
提升为一个 ClockWidget
为了能够使用 Designer 的这一特性,您必须提供一个采用QWidget
指针的构造器,并使 make 系统可以访问包含文件。这可以通过 QMake 项目文件中的INCLUDEPATH
变量来完成。
重要的是选择一个在定制部件继承树中的部件,以确保 Designer 中显示的所有属性都可以用于您的部件。用户界面编译器生成代码,用于设置设计器中标记为粗体的所有属性。在图 6-8 所示的属性框中,将设置objectName
、geometry
、text
和flat
属性。这意味着如果您升级小部件,您的小部件需要有setObjectName
、setGeometry
、setText
和setFlat
方法。如果您选择从自定义小部件的继承树中升级小部件,您可以通过继承免费获得这些方法。
图 6-8。 标记为粗体的属性将在 uic
生成的代码中设置。
提供插件
如果你花稍微多一点的时间实现一个在 Designer 中工作的插件,你可以跳过 Designer 中的提升方法。相反,您的小部件将与所有其他小部件一起出现在小部件框中。
为 Designer 创建一个插件几乎是一个复制粘贴的工作。在开始创建插件之前,您必须对小部件类声明做一个小小的修改。(对于插件,您将使用本章前面开发的CircleBar
小部件。)类声明如清单 6-25 中的所示。变化的前半部分是添加了QDESIGNER_WIDGET_EXPORT
宏,这确保了该类可以在 Qt 支持的所有平台上的插件中使用。另一半是添加一个以父代作为参数的构造器。这是从uic
生成的代码工作所需要的。
清单 6-25。 修改为 CircleBar
类
class QDESIGNER_WIDGET_EXPORT CircleBar : public QWidget
{
Q_OBJECT
public:
CircleBar( QWidget *parent = 0 );
CircleBar( int value = 0, QWidget *parent = 0 );
int value() const;
int heightForWidth( int ) const;
QSize sizeHint() const;
public slots:
void setValue( int );
signals:
void valueChanged( int );
protected:
void paintEvent( QPaintEvent* );
void wheelEvent( QWheelEvent* );
private:
int m_value;
};
现在你可以开始查看清单 6-26 中的实际插件了。plugin 类只是由QDesignerCustomWidgetInterface
类定义的接口的一个实现。所有的方法都必须实现,每个方法的任务都有严格的定义。
CircleBar
小部件的插件类叫做CircleBarPlugin
。这是命名小部件插件类的常见方式。
清单 6-26。 插件类
#ifndef CIRCLEBARPLUGIN_H
#define CIRCLEBARPLUGIN_H
#include <QDesignerCustomWidgetInterface>
class QExtensionManager;
class CircleBarPlugin : public QObject, public QDesignerCustomWidgetInterface
{
Q_OBJECT
Q_INTERFACES(QDesignerCustomWidgetInterface)
public:
CircleBarPlugin( QObject *parent = 0 );
bool isContainer() const;
bool isInitialized() const;
QIcon icon() const;
QString domXml() const;
QString group() const;
QString includeFile() const;
QString name() const;
QString toolTip() const;
QString whatsThis() const;
QWidget *createWidget( QWidget *parent );
void initialize( QDesignerFormEditorInterface *core );
private:
bool m_initialized;
};
#endif /* CIRCLEBARPLUGIN_H */
首先,小部件必须处理一个初始化标志,这是通过构造器和isInitialized()
和initialize(QDesignerFormEditorInterface*)
方法完成的。方法如清单 6-27 所示。您可以看到实现非常简单,可以在所有小部件插件类之间复制和粘贴。
清单 6-27。 移交初始化
CircleBarPlugin::CircleBarPlugin( QObject *parent )
{
m_initialized = false;
}
bool CircleBarPlugin::isInitialized() const
{
return m_initialized;
}
void CircleBarPlugin::initialize( QDesignerFormEditorInterface *core )
{
if( m_initialized )
return;
m_initialized = true;
}
如果你认为初始化标志处理很简单,你会发现清单 6-28 中的方法甚至更简单。方法isContainer()
、icon()
、toolTip()
、whatsThis()
尽可能少返回。您可以轻松地为您的小部件提供自定义图标、工具提示和这是什么文本。
清单 6-28。 返回最不可能的简单方法
bool CircleBarPlugin::isContainer() const
{
return false;
}
QIcon CircleBarPlugin::icon() const
{
return QIcon();
}
QString CircleBarPlugin::toolTip() const
{
return "";
}
QString CircleBarPlugin::whatsThis() const
{
return "";
}
includeFile()
、name()
和domXml()
方法返回从类名构建的标准化字符串。从name
和domXml
方法返回相同的类名是很重要的。请注意,该名称区分大小写。你可以在清单 6-29 中看到这些方法。
清单 6-29。 返回小部件的 XML、头文件名称和类名
QString CircleBarPlugin::includeFile() const
{
return "circlebar.h";
}
QString CircleBarPlugin::name() const
{
return "CircleBar";
}
QString CircleBarPlugin::domXml() const
{
return "<widget class=\"CircleBar\" name=\"circleBar\">\n"
"</widget>\n";
}
为了控制小部件出现在哪个小部件组中,从group()
方法返回组名。方法实现如清单 6-30 所示。
清单 6-30。 集团加盟设计师
QString CircleBarPlugin::group() const
{
return "Book Widgets";
}
为了帮助设计者创建一个小部件,你需要实现一个工厂方法,名为createWidget(QWidget*)
,如清单 6-31 所示。
清单 6-31。 创建小工具实例
QWidget *CircleBarPlugin::createWidget( QWidget *parent )
{
return new CircleBar( parent );
}
最后一步是使用Q_EXPORT_PLUGIN2
宏将插件类实际导出为插件,如清单 6-32 所示。这一行被添加到实现文件的末尾。
清单 6-32。 导出插件
Q_EXPORT_PLUGIN2( circleBarPlugin, CircleBarPlugin )
要构建一个插件,你必须创建一个特殊的项目文件,如清单 6-33 所示。清单中突出显示了重要的行。他们所做的是告诉 QMake 使用一个模板来构建一个库;然后CONFIG
行告诉 QMake 你需要designer
和plugin
模块。最后一行使用DESTDIR
变量配置构建的输出,以在正确的位置结束。
清单 6-33。 设计器插件的项目文件
TEMPLATE = lib
CONFIG += designer plugin release
DEPENDPATH += .
TARGET = circlebarplugin
HEADERS += circlebar.h circlebarplugin.h
SOURCES += circlebar.cpp circlebarplugin.cpp
DESTDIR = $$[QT_INSTALL_DATA]/plugins/designer
构建插件后,您可以通过访问 Help About Plugins 菜单项来检查 Designer 是否找到了插件。这将弹出如图图 6-9 所示的对话框。在图中,您可以看到插件已经加载,小部件已经找到。
图 6-9。 插件已经加载。
为 Designer 创建小部件插件只是简单地填写一个给定的界面。这项工作很容易,但也可能相当乏味。
总结
自定义小部件使您的应用程序与众不同。您的应用程序将执行的特殊任务通常通过一个特殊的小部件来处理。尽管如此,我还是建议您尽可能选择标准的小部件,因为应用程序的用户很难学会如何使用您的特殊小部件。
设计适合 Qt 编写应用程序方式的小部件并不难。首先,您需要找到一个要继承的小部件——起点。如果没有给定的起点,就得从QWidget
类开始。
选好合适的切入点后,你必须决定你要关注哪些事件。这有助于您决定要覆盖哪些事件处理函数。事件处理程序可以被认为是您与用户的接口。
当您决定了接口之后,您需要关注应用程序的其余部分,包括 setters、getters、signals 和 slots(以及设置大小策略和创建大小提示)。确保考虑除当前场景之外的使用场景,以使您的小部件可重用。在编写小部件时投入时间可以在未来的项目中帮助您,因为您可以避免一次又一次地重新发明轮子。
在讨论了所有这些软件开发问题之后,我必须强调小部件最重要的方面:可用性。试着从用户的角度思考,确保在把你的设计放到你的产品软件中之前,在真实用户身上测试你的设计。******
七、绘图和打印
Qt 中的一个 ll 绘画是通过QPainter
类以这样或那样的方式执行的。小部件、图片、代理——一切都使用相同的机制。该规则实际上有一个例外(直接使用 OpenGL),但是您将从QPainter
类开始。
绘制小工具
使用 Qt,你几乎可以在任何东西上绘图:小部件、图片、位图、图像、打印机、OpenGL 区域等等。所有这些 drawables 的公共基类是QPaintDevice
类。
因为小部件是一个绘画设备,所以您可以很容易地创建一个QPainter
用于在小部件上绘画;简单地将this
作为参数传递给构造器,如清单 7-1 所示。
清单 7-1。 将 this
作为参数从画图事件处理程序传递给 QPainter
构造器来设置一切。
void CircleBar::paintEvent( QPaintEvent *event )
{
...
QPainter p( this );
...
}
要为另一个 paint 设备设置 painter,只需将指向它的指针传递给 painter 构造器。清单 7-2 展示了如何设置一个点阵图的绘制器。创建了 200 像素宽和 100 像素高的位图。然后,创建了用于在像素图上绘图的画师,并设置了钢笔和画笔。钢笔是用来画你正在画的任何形状的边界的。画笔用于填充形状的内部。
在继续之前,你需要知道什么是点阵图,它和图片有什么不同。在 Qt 中有三个主要的类来表示图形:QPixmap
为在屏幕上显示而优化,QImage
为加载和保存图像而优化,QPicture
记录画师命令,并使其可以在以后重放。
提示 当针对 Unix 和 X11 时,QPixmap
类被优化为仅在屏幕上显示。它甚至可以存储在 X 服务器上(处理屏幕),这意味着应用程序和 X 服务器之间的通信更少。
清单 7-2。 在设置钢笔和画笔之前创建点阵图和画师
QPixmap pixmap( 200, 100 );
QPainter painter( &pixmap );
painter.setPen( Qt::red );
painter.setBrush( Qt::yellow );
...
清单 7-2 将钢笔和画笔设置为 Qt 的标准颜色——在本例中是红色钢笔和黄色画笔。可以通过QColor
类的构造器从红色、绿色和蓝色组件中创建颜色。您可以使用静态方法QColor::fromHsv
和QColor::fromCmyk
从色调、饱和度和值创建颜色;或者青色、品红色、黄色和黑色。Qt 还支持 alpha 通道,控制每个像素的不透明度。(在本章的后面,您将对此进行实验。)
如果你想清除画笔和画笔设置,你可以使用setPen(Qt::noPen)
和setBrush(Qt::noBrush)
调用。钢笔用来画出形状的轮廓,而刷子用来填充它们。因此,你不用画笔就可以画出轮廓,不用钢笔就可以填充形状。
绘图操作
painter 类使您能够绘制您可能需要的最基本的形状。本节列出了最有用的方法以及示例输出。首先让我们看看几个经常被用作绘图方法参数的类。
画画时,你必须告诉画家在哪里画形状。屏幕上的每个点都可以用一个 x 和一个 y 值来指定,如图图 7-1 所示。如您所见,y 轴从顶部开始,其中 y 为 0,向下到更高的值。同理,x 轴从左到右增长。当谈论一个点时,你写( x,y )。这意味着(0,0)是坐标系的左上角。
注意可以使用负坐标移动到(0,0)位置的上方和左侧。
**图 7-1。**x 值从左到右递增;y 值从顶部向下增加。
图 7-2 显示了在小部件上绘图时,小部件的坐标系如何不同于屏幕。在小部件上绘图时使用的坐标是对齐的,因此(0,0)是小部件的左上角(在设备的全局坐标系中,它不总是与(0,0)相同)。全局坐标系处理屏幕上的实际像素、打印机上的点和其他设备上的点。
图 7-2。 在小部件上绘图时,小部件的左上角为(0,0)。
屏幕上的一个点由一个QPoint
对象表示,你可以在构造器中为一个点指定 x 和 y 的值。一个点通常不足以画出什么东西;要指定一个点的宽度和高度,可以使用QRect
类。QRect
构造器接受一个 x 值、一个 y 值和一个宽度,后跟一个高度。图 7-3 显示了一个坐标系中的QRect
和QPoint
。
图 7-3。 一个 QPoint
和一个 QRect
及其 x、y、宽度和高度属性
提示与QPoint
和QRect
密切相关的有两类:QPointF
和QRectF
。它们是等效的,但是对浮点值进行操作。几乎所有接受矩形或点的方法都可以接受任何类型的矩形或点。
行
线条是最基本的形状,你可以用画师画出来。使用drawLine(QPoint,QPoint)
方法在两点之间画一条线。如果想一次加入更多的点,可以用drawPolyline(QPoint*, int)
的方法。drawLines(QVector
< QPoint
>)方法也用于一次绘制多条线,但这些线不是连续的。这三种方法在清单 7-3 中使用,结果显示在图 7-4 中。
在清单中,创建了一个 pixmap,并在创建 painter 之前用白色填充,笔被配置为绘制黑色线条。两个向量polyPoints
和linePoints
被初始化,其中linePoints
通过将polyPoints
点向右移动 80 个像素来计算。您可以通过向每个QPoint
添加偏移QPoint
来移动这些点,这将分别将 x 和 y 值相加。
注意我把polyPoints
称为矢量,因为这才是QPolygon
真正的含义。然而,QPolygon
类也提供了同时移动所有点的方法,以及计算包含所有点的矩形的方法。
为了绘制实际的线条,调用了drawLine
、drawPolyline
和drawLines
方法。比较一下drawPolyline
和drawLines
的区别。如你所见,drawPolyline
连接所有的点,而drawLines
连接每一对给定的点。
清单 7-3。 利用drawLine``drawPolyline
drawLines
绘制线条
QPixmap pixmap( 200, 100 );
pixmap.fill( Qt::white );
QPainter painter( &pixmap );
painter.setPen( Qt::black );
QPolygon polyPoints;
polyPoints << QPoint( 60, 10 )
<< QPoint( 80, 90 )
<< QPoint( 75, 10 )
<< QPoint( 110, 90 );
QVector<QPoint> linePoints;
foreach( QPoint point, polyPoints )
linePoints << point + QPoint( 80, 0 );
painter.drawLine( QPoint( 10, 10 ), QPoint( 30, 90 ) );
painter.drawPolyline( polyPoints );
painter.drawLines( linePoints );
图 7-4。 用不同的方法绘制线条;从左至右:drawLine``drawPolylines
drawLines
(两行)**
*线条是使用钢笔绘制的,因此您可以通过改变钢笔对象的属性来绘制所需的线条。一个QPen
对象最常用的两个属性是color
和width
,它们控制所画线条的颜色和宽度。
当使用drawPolyline
绘制连续线条时,能够控制线条如何连接在一起是很有用的—joinStyle
属性会有所帮助。图 7-5 显示了可用的样式:斜面、斜接和圆形。通过将您的QPen
对象的joinStyle
设置为Qt::BevelJoin
、Qt::MiterJoin
或Qt::RoundJoin
来设置合适的样式。
图 7-5。 线段有三种连接方式:斜角、斜接和圆角。
QPen
可被设置成画点划线以及完全自由的虚线。图 7-6 中显示了不同的变化。
图 7-6。 线条可以用不同的图案绘制成实线或虚线——既有预定义的图案,也有定制图案的功能。
通过将QPen
对象的style
属性设置为Qt::SolidLine
、Qt::DotLine
、Qt::DashLine
、Qt::DotDashLine
、Qt::DotDotDashLine
或Qt::CustomDashLine
来选取图案。如果您使用自定义线条,您还必须通过dashPattern
属性设置一个自定义的虚线图案(清单 7-4 显示了它是如何完成的)。列表的输出如图 7-7 所示。
dashPattern
由qreal
值的向量列表组成。这些值决定了破折号和间隙的宽度,其中第一个值是第一个破折号,然后是一个间隙,然后是一个破折号,然后是另一个间隙,依此类推。
清单 7-4。 使用预定义或自定义图案绘制线条
QPixmap pixmap( 200, 100 );
pixmap.fill( Qt::white );
QPainter painter( &pixmap );
QPen pen( Qt::black );
pen.setStyle( Qt::SolidLine );
painter.setPen( pen );
painter.drawLine( QPoint( 10, 10 ), QPoint( 190, 10 ) );
pen.setStyle( Qt::DashDotLine );
painter.setPen( pen );
painter.drawLine( QPoint( 10, 50 ), QPoint( 190, 50 ) );
pen.setDashPattern( QVector<qreal>() << 1 << 1 << 1 << 1 << 2 << 2
<< 2 << 2 << 4 << 4 << 4 << 4
<< 8 << 8 << 8 << 8 );
pen.setStyle( Qt::CustomDashLine );
painter.setPen( pen );
painter.drawLine( QPoint( 10, 90 ), QPoint( 190, 90 ) );
图 7-7。 预定义和自定义图案
方形形状
可以画方形或圆角的矩形,如图图 7-8 所示。这些方法接受代表左上角( x,y )对的一个QRect
或四个值,然后是矩形的宽度和高度。这些方法被命名为drawRect
和drawRoundRect
。
图 7-8。 圆角矩形
清单 7-5 展示了圆角矩形和方角矩形是如何绘制的。前两个矩形是使用方法调用中直接指定的坐标绘制的。坐标指定为 x,y,w,h;其中 x 和 y 指定左上角, w , h 指定矩形的宽度。
注意如果 w 或 h 小于 0,则 x,y 指定的角不是矩形的左上角。
第二对矩形是根据给定的QRect
类绘制的,该类保存矩形的坐标。在drawRoundRect
调用中,直接使用了rect
变量。在drawRect
调用中,rect
指定的矩形被平移,或者向下移动 45 个像素。这是通过使用translated(int x, int y)
方法实现的,该方法返回一个相同大小的矩形,但是移动了指定的像素数量。
绘图操作的结果如图 7-9 所示。
清单 7-5。 将矩形绘制成点阵图
QPixmap pixmap( 200, 100 );
pixmap.fill( Qt::white );
QPainter painter( &pixmap );
painter.setPen( Qt::black );
painter.drawRect( 10, 10, 85, 35 );
painter.drawRoundRect( 10, 55, 85, 35 );
QRect rect( 105, 10, 85, 35 );
painter.drawRoundRect( rect );
painter.drawRect( rect.translated( 0, 45 ) );
图 7-9。 所画的矩形
圆形形状
使用drawEllipse
方法绘制圆和椭圆(参见图 7-10 )。该方法采用一个矩形或四个值来表示 x,y ,宽度和高度(就像矩形绘制方法一样)。要画一个圆,你必须确保宽度和高度相等。
图 7-10。 使用 drawEllipse
方法绘制圆和椭圆。
画椭圆很有趣,因为你还可以画出椭圆的一部分。Qt 可以绘制三个部分(如图图 7-11 ):
drawArc
画一条弧线——圆圈周围的线条部分。drawChord
画一个圆段——弦与弦外圆弧之间的区域。drawPie
画一个扇形段——椭圆形的一部分。
所有绘制椭圆部分的方法都取一个矩形(就像drawEllipse
方法一样)。然后,它们接受一个起始角度和一个值,该值指示椭圆的一部分跨越了多少度。角度以整数表示,其中值为 1/16 度,这意味着值 5760 对应于一个完整的圆。值 0 对应三点钟,正角度逆时针移动。
图 7-11。 一条弧线、一条弦和一个饼状的圆
清单 7-6 展示了如何绘制椭圆和圆弧(结果如图图 7-12 )。正如您所看到的,形状的比例发生了变化,最右边的椭圆和圆弧实际上是圆形的(宽度等于高度)。
如源代码所示,可以通过直接使用坐标或向绘图方法传递一个QRect
值来指定绘制椭圆或圆弧的矩形。
在指定角度时,我将不同的值乘以 16,将实际角度值转换为 Qt 期望的值。
清单 7-6。 画椭圆和圆弧
QPixmap pixmap( 200, 190 );
pixmap.fill( Qt::white );
QPainter painter( &pixmap );
painter.setPen( Qt::black );
painter.drawEllipse( 10, 10, 10, 80 );
painter.drawEllipse( 30, 10, 20, 80 );
painter.drawEllipse( 60, 10, 40, 80 );
painter.drawEllipse( QRect( 110, 10, 80, 80 ) );
painter.drawArc( 10, 100, 10, 80, 30*16, 240*16 );
painter.drawArc( 30, 100, 20, 80, 45*16, 200*16 );
painter.drawArc( 60, 100, 40, 80, 60*16, 160*16 );
painter.drawArc( QRect( 110, 100, 80, 80 ), 75*16, 120*16 );
图 7-12。 所画的椭圆和圆弧
正文
Qt 提供了几种可能的方法来绘制文本(参见图 7-13 中的一些例子)。当您通过用于创建它的代码工作时,请参考该图。
图 7-13。 你可以用许多不同的方式绘制文本。
首先,您需要创建一个用于绘制的QPixmap
和一个用于绘制的QPainter
。你还必须用白色填充位图,并将画师的笔设置为黑色:
QPixmap pixmap( 200, 330 );
pixmap.fill( Qt::white );
QPainter painter( &pixmap );
painter.setPen( Qt::black );
在图的顶部绘制文本,该文本从 a `QPoint`开始。下面的源代码向您展示了`drawText`调用的使用。下面的`drawLine`类简单地用十字标记了点(你可以在顶部文本左边的图 7-13 中看到这个十字)。
QPoint point = QPoint( 10, 20 );
painter.drawText( point, "You can draw text from a point..." );
painter.drawLine( point+QPoint(-5, 0), point+QPoint(5, 0) );
painter.drawLine( point+QPoint(0, −5), point+QPoint(0, 5) );
从一个点绘制文本有它的优势——这是一种将文本放到屏幕上的简单方法。如果您需要更多的控制,您可以在矩形中绘制文本,这意味着您可以将文本水平向右、向左或居中对齐(也可以垂直向上、向下或居中)。下表总结了用于对齐的枚举:
Qt::AlignLeft
:左对齐Qt::AlignRight
:右对齐Qt::AlignHCenter
:居中水平对齐Qt::AlignTop
:顶部对齐Qt::AlignBottom
:底部对齐Qt::AlignVCenter
:垂直居中对齐Qt::AlignCenter
:垂直和水平居中对齐
在矩形内绘制文本的另一个好处是文本被裁剪到矩形,这意味着您可以限制文本使用的区域。下面的源代码绘制了一个以矩形为中心的文本:
QRect rect = QRect(10, 30, 180, 20); painter.drawText( rect, Qt::AlignCenter, "...or you can draw it inside a rectangle." ); painter.drawRect( rect );
因为您可以将文本限制为矩形,所以您还需要能够确定文本使用了多少空间。首先将矩形平移到一个新位置;您将从QApplication
对象中获得标准的QFont
。使用字体,设置一个pixelSize
来适应矩形,然后在矩形的两边绘制文本。
提示因为你在给一个QPixmap
绘画,所以使用来自QApplication
的字体。如果你在一个特定的小部件中使用的QWidget
或QPixmap
上绘画,从小部件中获取字体会更合理。
这并没有像预期的那样结束;相反,文本在底部被剪裁。字体的像素大小只定义了所有字符绘制的基线以上的大小。
` rect.translate( 0, 30 );
QFont font = QApplication::font();
font.setPixelSize( rect.height() );
painter.setFont( font );
painter.drawText( rect, Qt::AlignRight, “Right.” );
painter.drawText( rect, Qt::AlignLeft, “Left.” );
painter.drawRect( rect );`
要真正使文本适合矩形,使用QFontMetrics
类获得文本的精确尺寸。font metrics 类可用于确定给定文本的宽度和高度。然而,高度不依赖于任何特定的文本;它完全由字体决定。下面的代码在绘制文本之前调整用于保留文本的矩形的高度。参见图 7-13 :这一次文本非常合适。
` rect.translate( 0, rect.height()+10 );
rect.setHeight( QFontMetrics( font ).height() );
painter.drawText( rect, Qt::AlignRight, “Right.” );
painter.drawText( rect, Qt::AlignLeft, “Left.” );
painter.drawRect( rect );`
使用drawText
绘制文本有其局限性。例如,部分文本不能格式化,也不能分成段落。您可以使用QTextDocument
类来绘制格式化的文本(如下面的源代码所示)。
用文本文档绘制文本比直接使用drawText
稍微复杂一些。首先创建一个QTextDocument
对象,使用setHTML
用 HTML 格式的文本初始化它。设置要在其中绘制文本的矩形。将其平移到最后绘制的文本下方的新位置,然后调整高度以容纳更多文本。
然后使用setTextWidth
将矩形用于设置文本文档的宽度。在您准备绘制文本之前,您必须翻译 painter(稍后将详细介绍),因为文本文档将在(0,0)坐标处开始绘制文本。在翻译 painter 之前,保存当前状态(稍后通过调用restore
方法恢复)。因为您翻译了画师,所以当您调用drawContents
在给定的矩形内将文本绘制给给定的画师时,您也必须翻译矩形。
` QTextDocument doc;
doc.setHtml( "
A QTextDocument can be used to present formatted text "
“in a nice way.
"
It can be formatted "
“in different ways.
"
The text can be really long and contain many "
“paragraphs. It is properly wrapped and such…
rect.translate( 0, rect.height()+10 );
rect.setHeight( 160 );
doc.setTextWidth( rect.width() );
painter.save();
painter.translate( rect.topLeft() );
doc.drawContents( &painter, rect.translated( -rect.topLeft() ) );
painter.restore();
painter.drawRect( rect );`
如图图 7-13 所示,文本文档的全部内容无法放入给定的矩形。再次,有一种方法可以确定文本所需的高度。在这种情况下,使用来自QTextDocument
的size
属性的height
属性。在下面的源代码中,您使用此高度来确定绘制在呈现的文本文档下方的灰色矩形的大小。这个矩形显示了文本的实际长度。
rect.translate( 0, 160 ); rect.setHeight( doc.size().height()-160 ); painter.setBrush( Qt::gray ); painter.drawRect( rect );
注意尽管使用drawText
方法绘制文本相当容易,但是您可能想要使用QTextDocument
类来绘制更复杂的文本。这个类使您能够以一种简单的方式绘制具有各种格式和对齐方式的复杂文档。
路径
画家路径可以画出你想要的任何形状,但技巧是定义一个区域周围的路径。然后,您可以使用给定的钢笔和画笔来绘制路径。一条路径可以包含几个封闭区域;例如,可以使用路径来表示整个文本字符串。
如图图 7-14 所示的路径分三步创建。首先,创建QPainterPath
对象,并使用addEllipse
方法添加圆。这个椭圆形成一个封闭的区域。
` QPainterPath path;
path.addEllipse( 80, 80, 80, 80 );`
图 7-14。 一条小路已经被填满。
下一步是添加从整个圆的中心开始并向左上方延伸的四分之一圆。它从(100,100)开始,使用一个moveTo
调用移动到那个点。然后你用lineTo
画一条直线,然后用addArc
画一个圆弧。从(40,40)开始在矩形中绘制圆弧;也就是 160 像素的高和宽。它从 90 度开始,逆时针再跨越 90 度。然后用一条返回起点的线封闭该区域。这形成了另一个封闭区域。
注意圆弧从 90 度开始,因为 0 度被认为是中心点右侧的点,你希望它从中心正上方开始。
path.moveTo( 120, 120 ); path.lineTo( 120, 40 ); path.arcTo( 40, 40, 160, 160, 90, 90 ); path.lineTo( 120, 120 );
最后要添加的部分是形状下面的文本。这是通过设置一个大字体,然后在调用addText
时使用它来实现的。addText
的工作方式类似于drawText
,但是只允许文本从给定点开始(也就是说,矩形中不包含文本)。这就形成了一大堆构成文本的封闭区域:
` QFont font = QApplication::font();
font.setPixelSize( 40 );
path.addText( 20, 180, font, “Path” );`
当画家路径完成后,剩下要做的就是用画家来描边。在下面的代码中,您为画家配置了一支钢笔和一支画笔。然后用drawPath
方法画出实际的油漆工路径。
图 7-14 显示当区域重叠时,笔刷没有被应用。这使得通过将其他路径放入其中来创建空心路径成为可能。
` painter.setPen( Qt::black );
painter.setBrush( Qt::gray );
painter.drawPath( path );`
路径可以由比前面源代码中使用的形状更多的形状组成。以下列表提到了一些可用于将形状添加到路径中的方法:
addEllipse
:添加一个椭圆或圆。addRect
:添加一个矩形。addText
:添加文本。addPolygon
:添加一个多边形。
从线、弧和其他组件构建区域时,以下方法会很有用:
moveTo
:移动当前位置。lineTo
:画一条线到下一个位置。arcTo
:画一条弧线到下一个位置。cubicTo
:画一条三次贝塞尔曲线(一条平滑的线)到下一个点。closeSubpath
:从当前位置到起点画一条直线,关闭当前区域。
路径对于表示你需要反复绘制的形状是非常有用的,但是当它们与画笔结合起来时,它们的真正潜力才会显现出来(接下来讨论)。
画笔
画笔用于填充形状和路径。到目前为止,你一直用画笔用纯色填充指定的区域。这只是可能的一部分。使用不同的图案、渐变甚至纹理,您可以用任何可以想象的方式填充形状。
创建QBrush
对象时,可以指定颜色和样式。构造器定义为QBrush(QColor, Qt::BrushStyle)
。然后使用setBrush
方法将QBrush
赋予一个QPainter
。
画笔的样式控制填充形状时如何使用颜色。最简单的样式是图案,在需要用线条或抖动阴影填充形状时使用。可用的模式和相应的枚举样式如图 7-15 所示。
图 7-15。 可用模式
一种更灵活的填充形状的方法是使用渐变画笔,这是一种基于QGradient
对象的画笔。一个渐变对象代表一种或多种颜色根据预定义的模式的混合。可用模式如图 7-16 中的所示。基于QLinearGradient
类的线性渐变定义了一个二维线性渐变。径向渐变通过QRadialGradient
实现,描述了从一个点发出的渐变,其中阴影取决于与该点的距离。圆锥形渐变,QConicalGradient
,代表从单个点发出的渐变,其中阴影取决于与该点的角度。
不同的渐变定义为两点之间的分布(锥形渐变除外,它以一个角度开始和结束)。梯度在这些点定义的范围之外继续的方式由扩展策略定义,该策略通过setSpread
方法设置。不同传播策略的结果也显示在图 7-16 中。使用垫展开 ( QGradient::PadSpread
),当到达垫时,梯度简单地停止。使用重复展开 ( QGradient::RepeatSpread
)梯度被重复。使用反射扩散 ( QGradient::ReflectSpread
)梯度重复,但方向是交替的——导致梯度每隔一次被反射。
注意扩散策略不会影响锥形渐变,因为它们定义了所有像素的颜色。
图 7-16。 不同的梯度和扩散政策
清单 7-7 显示了不同梯度是如何配置的。请注意,线性梯度是在两点之间定义的,形成一个方向。径向渐变由中心点和半径定义,而锥形渐变由中心点和起始角度定义。起始角度以度为单位指定,其中 0 度定义从中心点指向右侧的方向。
渐变也使用setColorAt
方法分配颜色。颜色被设置为范围在 0 和 1 之间的值。这些值为线性渐变定义了两点之间的一点,其中一点为 0,另一点为 1。同样,0 定义起点,1 定义径向渐变的完整指定半径。对于锥形渐变,0 指定起始角度。然后,该值沿逆时针方向增加,直到 1 指定结束角度,该角度与开始角度相同。
注意可以在不同的点设置几种颜色;设置结束颜色以清晰的方式显示效果。
清单 7-7。 设置渐变
` QLinearGradient linGrad( QPointF(80, 80), QPoint( 120, 120 ) );
linGrad.setColorAt( 0, Qt::black );
linGrad.setColorAt( 1, Qt::white );
…
QRadialGradient radGrad( QPointF(100, 100), 30 );
radGrad.setColorAt( 0, Qt::black );
radGrad.setColorAt( 1, Qt::white );
…
QConicalGradient conGrad( QPointF(100, 100), −45.0 );
conGrad.setColorAt( 0, Qt::black );
conGrad.setColorAt( 1, Qt::white );`
要将其中一个渐变用作画笔,只需将QGradient
对象传递给QBrush
构造器。渐变画笔不受调用QBrush
对象的setColor
方法的影响。
创建画笔的最后一种方法是将一个QPixmap
或一个QImage
对象传递给QBrush
构造器,或者在一个QBrush
对象上调用setTexture
。这个过程使画笔使用给定的图像作为纹理,通过重复图案填充任意形状(一个例子如图图 7-17 )。
图 7-17。 一个基于纹理的笔刷
改造现实
正如您在讨论全局(设备)坐标和局部(小部件)坐标时了解到的,Qt 可以对屏幕的不同区域使用不同的坐标系。全局坐标和局部坐标的区别在于原点,即点(0,0),被移动了。用专业术语来说,这就是所谓的坐标系统转换。
注意我将设备的坐标称为全局,因为它们是在设备上工作的所有画师(以及小部件,如果设备恰好是屏幕)之间共享的。然后,每个画家都被转变成一个与其目的相关的点。其他常用的符号有物理设备坐标和逻辑局部坐标。
油漆工的坐标系也可以被转换(这种转换的例子如图 7-18 中的所示)。在图中,灰色框是相对于原始坐标系绘制的。坐标系通过以下调用进行转换:
painter.translate( 30, 30 );
结果是在黑色矩形所在的位置绘制了矩形,坐标系向右下方移动了。
**图 7-18。** *平移坐标系就是移动原点(0,0)。*
painter 类能够进行更多的翻译。坐标系可以平移、缩放、旋转和剪切(这些变换如图 7-19 、 7-20 和 7-21 所示)。
为了缩放画师,进行以下调用:
painter.scale( 1.5, 2.0 );
第一个参数是沿 *x* 轴的缩放(水平方向),第二个参数是垂直缩放(见图 7-19 )。请注意,用于绘画的钢笔也被缩放—线条的高度大于宽度。
**图 7-19。** *缩放坐标系使所有点更靠近原点(0,0)。*
旋转时,会进行以下调用:
painter.rotate( 30 );
该参数是顺时针方向旋转坐标系的度数。该方法接受浮点值,因此可以将坐标系旋转任意角度(见图 7-20 )。
**图 7-20。** *绕原点(0,0)旋转坐标系*
最后一个变换——剪切——有点复杂。发生的情况是,坐标系绕原点扭曲。为了理解这一点,请看图 7-21 和下面的调用:
painter.shear( 0.2, 0.5 );
注意, *x* 值越大, *y* 值的变化越大。同样,大的 *y* 值会导致 *x* 值的大变化。`shear`方法的第一个参数控制 *y* 值的变化量 *x* 应该给出多大的变化量,第二个参数反过来做同样的事情。例如,查看被剪切的矩形的右下角,并将其与原始的灰色框进行比较。然后比较剪切后的矩形和原始矩形的左上角。比较这两个点,可以看到根据`shear`方法参数的大小,一个比另一个移动得多。因为右上角的 *x* 和 *y* 均为非 0 值,所以该点根据参数向两个方向移动。
**图 7-21。** *相对于原点(0,0)剪切坐标系*
当你对一个画师的坐标系进行变换时,你想知道有一种方法可以恢复原来的设置。通过调用 painter 对象上的`save`,当前状态被放在一个堆栈上。要恢复上一次保存的状态,调用`restore`(当您想要应用几个从原始坐标系开始的变换时,这很方便)。给定一个指向画师对象的指针也很常见;您应该在修改绘制器之前保存状态,然后在从方法返回之前恢复绘制器。
**维持秩序**
可以通过依次执行来组合几种转换。这样做时,顺序很重要,因为所有的变换都指向原点(0,0)。例如,旋转始终意味着围绕原点旋转,因此,如果您想围绕不同的点旋转形状,您必须将旋转中心平移到(0,0),应用旋转,然后将坐标系平移回来。
让我们用下面的线在(0,0)处画一个矩形,即 70 像素宽,70 像素高:
painter.drawRect( 0, 0, 70, −70 );
现在使用下面的线将坐标系旋转 45 度(结果如图 7-22 所示):
painter.rotate( 45 );
**图 7-22。** *简单地旋转矩形使其绕原点旋转。*
如果你转而平移坐标系,使矩形(35,35)的中心成为旋转前的原点,然后再将坐标系平移到位,你最终会像图 7-23 一样。用于平移和旋转然后平移回来的代码如下:
painter.translate( 35, −35 );
painter.rotate( 45 );
painter.translate( −35, 35 );
**图 7-23。** *通过来回平移,可以围绕矩形的中心旋转。*
如果你混淆了平移的顺序,你最终会得到图 7-24 (你绕着错误的点旋转)。
**图 7-24。** *混译的顺序绕着错误的原点旋转。*
翻译的顺序对所有的翻译都很重要。缩放和剪切都同样依赖于坐标系的原点,就像旋转一样。
绘画小工具
所有 Qt 小部件都是绘画设备,所以您可以创建一个`QPainter`对象,并用它来绘制小部件。然而,这只能通过`paintEvent(QPaintEvent*)`方法来实现。
当小部件需要重画时,事件循环调用`paintEvent`方法。你需要告诉 Qt 什么时候要重画小部件,Qt 会调用你的`paintEvent`方法。你可以用两种方法来达到这个目的:`update`和`repaint`。`repaint`方法触发并立即重画,而`update`将一个更新请求放到事件队列中。后者意味着 Qt 有机会将`update`调用合并成更少的(最好是单个)对`paintEvent`的调用。这可能是好的也可能是坏的。这很糟糕,因为您可能已经创建了一个依赖于`paintEvent`被调用特定次数的小部件。这很好,因为它允许 Qt 根据运行应用程序的系统的当前工作负载来调整数量或重画。几乎在所有情况下,你都应该使用`update`。这样做时,尽量避免依赖于被调用一定次数的`paintEvent`方法。
**注意**有更多的理由不依赖于`paintEvent`像你呼叫`update`那样被频繁呼叫。例如,你的小工具可能会被完全阻挡,或者有什么东西在它前面移动,导致对`paintEvent`的呼叫变少或变多。
在你忘乎所以并开始实现全新的小部件之前,让我们来看看一个按钮是如何被修改成看起来不同的。(按钮是一个很好的起点,因为它就是为此目的而设计的。)所有按钮都继承了`QAbstractButton`类,它定义了按钮的基本机制和属性。然后这个类被继承到`QPushButton`、`QRadioButton`和`QCheckBox`中,它们实现了一个按钮的三种不同视图。
**注意**还有更多抽象的 widget 可以作为自定义 widget 的基础,包括`QAbstractScrollArea`、`QAbstractSlider`和`QFrame`。注意,尽管前两个类是抽象的,但这不是规则。`QFrame`可以作为新 widget 的基础,但也可以单独使用。
**一个新按钮**
新的 button 类并没有创建一个完全不同的按钮;当用户按下按钮时,它只是让按钮的文本变亮。按钮类被称为`MyButton`,类声明如清单 7-8 所示。
在清单中,您可以看到该类继承了`QAbstractButton`类。然后它实现一个构造器、一个`sizeHint`方法和一个`paintEvent`方法。`sizeHint`和`paintEvent`方法覆盖从祖先类继承的现有方法。这意味着它们的声明必须保持完全相同(包括将`sizeHint`方法声明为`const`)。
**清单 7-8。** *自定义按钮的类声明*
class MyButton : public QAbstractButton
{
Q_OBJECT
public:
MyButton( QWidget *parent=0 );
QSize sizeHint() const;
protected:
void paintEvent( QPaintEvent* );
};
你可以在清单 7-9 中查看构造器和`sizeHint`方法。构造器只是将父参数传递给父类。`sizeHint`方法返回小部件想要的大小。这只是给 Qt 布局类的一个提示,所以不能依赖小部件来获取这些尺寸。
尺寸由`QSize`对象表示,它有两个属性:`width`和`height`。对于按钮,这两个度量取决于要显示的文本和显示文本所用的字体。要了解给定`QFont`的尺寸,请使用`QFontMetrics`对象。所有小部件都有一个返回当前字体的`QFontMetrics`对象的`fontMetrics`属性。通过询问这个对象关于给定字符串的`width`和`height`的信息,然后在每个方向上额外增加 10 个像素作为边距,就可以得到一个合适的小部件大小。
**注意**给定字体的高度并不取决于输入的文本。相反,它考虑了字体的可能高度。大多数字体的给定文本的宽度取决于文本,因为字符的宽度不同。
**清单 7-9。** *构造器和* `sizeHint` *按钮的方法*
MyButton::MyButton( QWidget *parent ) : QAbstractButton( parent )
{
}
QSize MyButton::sizeHint() const
{
return QSize( fontMetrics().width( text() )+10, fontMetrics().height()+10 );
}
绘制按钮的任务由`paintEvent`方法负责(见清单 7-10 )。该方法从创建一个用于绘制到小部件的`QPainter`对象开始。所有的小部件都由 Qt 进行双缓冲,所以当你向画师绘图时,你实际上是在向一个用于重绘屏幕的缓冲区绘图。这意味着你不必担心闪烁。
有两种方法可以绘制小部件:直接绘制或通过样式绘制。通过使用样式,您可以使小部件的外观适应系统的其余部分。通过直接绘制到小部件,您可以完全控制。对于按钮,您将直接使用样式和文本绘制框架和背景。
每个小部件都有一个与之相关联的`QStyle`,您可以通过`style`属性访问它。这种风格通常反映了系统的设置,但是它可能已经从实例化小部件的代码中改变了。小部件本身不应该关心样式的起源或者它与当前平台的关系。
在使用该样式进行绘图之前,您需要设置一个样式选项对象(在本例中是一个`QStyleOptionButton`对象)。要使用的样式选项类取决于要绘制的样式元素。通过参考用于`drawControl`方法的 Qt 文档,您可以看到它期望哪个样式对象。
样式选项对象通过将`this`指针传递给它的`init`方法来初始化,该方法配置大多数设置。但是,您仍然需要判断按钮是被按下还是被切换。这些状态可以从由`QAbstractButton`类实现的`isDown`和`isChecked`方法中获得。如果`isDown`方法返回`true`,则按钮当前被按下。如果`isChecked`返回`true`,则按钮已被切换且当前处于选中状态(即处于打开状态)。当按钮被按下时,设置样式选项的`state`属性中的`QStyle::State_Sunken`位。对于选中的按钮,设置`QStyle::State_On`位。
**注意**使用`|=`操作符(按位或)将`state`位相加,不清除由`init`方法设置的任何位。
当样式对象被正确设置后,当前样式方法的`drawControl(ControlElement, QStyleOption*, QPainter*, QWidget*)`被调用。在调用中,您要求绘制一个`QStyle::CE_PushButtonBevel`,它将绘制按钮的所有部分,除了文本和可选图标。
`paintEvent`方法的第二部分负责将文本直接绘制到小部件上。它首先将画师的字体设置为小部件的当前字体。然后根据按钮的状态确定笔的颜色。禁用的按钮显示灰色文本,按下的按钮显示红色文本,所有其他按钮显示暗红色文本。请注意,当按钮被主动按下时,`isDown`会返回`true`,而不是当切换的按钮处于打开状态时。这意味着文本只有在按下鼠标按钮时才会变亮。
当画师的笔和字体配置好后,继续用`drawText`绘制实际文本。文本在按钮中居中,并包含在按钮所占据的实际矩形中。你没有考虑在`sizeHint`方法中添加的边距。
`paintEvent`方法接受一个`QPaintEvent`指针作为参数;在本例中选择忽略的指针。事件对象有一个名为`rect()`的成员方法,它返回一个`QRect`,指定`paintEvent`方法需要更新的矩形。对于某些小部件,您可以将绘制限制在该矩形内,以提高性能。
**清单 7-10。** *直接使用样式和文本绘制斜面*
void MyButton::paintEvent( QPaintEvent* )
{
QPainter painter( this );
QStyleOptionButton option;
option.init( this );
if( isDown() )
option.state |= QStyle::State_Sunken;
else if( isChecked() )
option.state |= QStyle::State_On;
style()->drawControl( QStyle::CE_PushButtonBevel, &option, &painter, this );
painter.setFont( font() );
if( !isEnabled() )
painter.setPen( Qt::darkGray );
else if( isDown() )
painter.setPen( Qt::red );
else
painter.setPen( Qt::darkRed );
painter.drawText( rect(), Qt::AlignCenter, text() );
}
要试用这个按钮,可以用它创建一个对话框。产生的对话框如图 7-26 中的所示(但你离它还有几步之遥)。
首先在设计器中创建新对话框。向对话框添加三个`QPushButton`小部件,并根据对话框的图形设置它们的文本属性。同样,将顶部按钮的 enabled 属性设置为`false`,将底部按钮的 checkable 属性设置为`true`。
右键单击每个按钮,并从弹出菜单中选择“升级到自定义小部件”。这将显示在图 7-25 中弹出菜单旁边显示的对话框。通过在对话框中输入`MyButton`作为自定义类名,头文件名称将(正确地)被猜测为`mybutton.h`,这将导致用户界面编译器在创建按钮时使用`MyButton`类,而不是`QPushButton`类。
**注意**因为`MyButton`没有继承`QPushButton`(它继承了`QAbstractButton`类),所以保持属性编辑器中`QPushButton`标题下的属性不变是很重要的。否则,您将会遇到编译错误。基类(`QAbstractButton`)及以上的所有属性都可以自由使用。
对话框名称设置为`Dialog`,中间按钮命名为`clickButton`,设计保存为`dialog.ui`。
**图 7-25。** *使用来自设计者*的 `MyButton` *为了显示对话框,声明一个最小的对话框类(如清单 7-11 中的和清单 7-12 中的所示)。对话框简单地从设计中设置用户界面,并将按钮的`clicked`信号连接到显示对话框的插槽。
**清单 7-11。** *一个极小对话框的标题*
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog();
private slots:
void buttonClicked();
private:
Ui::Dialog ui;
};
**清单 7-12。** *实现一个最小化的对话框*
Dialog::Dialog() : QDialog()
{
ui.setupUi( this );
connect( ui.clickButton, SIGNAL(clicked()), this, SLOT(buttonClicked()) );
}
void Dialog::buttonClicked()
{
QMessageBox::information( this, tr(“Wohoo!”), tr(“You clicked the button!”) );
}
该对话框与一个最小的主函数相结合,产生了如图 7-26 所示的对话框。在图中,顶部的按钮被禁用,中间的按钮被按下,而底部的按钮是非活动的切换按钮。

**图 7-26。***`MyButton`*类行动**
***完全定制**
如果您需要创建一个全新的小部件(其行为不同于任何其他小部件),您必须直接子类化`QWidget`类。这让你可以做任何事情,但是自由也伴随着责任。所有的内部状态都必须由你来管理,所有的重画和大小暗示也是如此。
让我们先看看你想做什么。您将创建的小部件名为`CircleWidget`,它将监听鼠标事件。当按下鼠标时,会创建一个圆。只要在圆圈内按下鼠标按钮,圆圈就会变大。如果在指针位于圆圈之外时按下鼠标,圆圈将缩小直至消失,新的圆圈将在第一个圆圈消失时指针所在的位置开始增长(见图 7-27 )。

**图 7-27。** *圆形小部件显示的圆形*
你必须跟踪鼠标事件:按钮按下、按钮释放和指针移动。你还需要有一个计时器,随着时间的推移来扩大和缩小圆圈。最后,你必须负责重画并给 Qt 布局类一个大小提示(所有这些都可以在清单 7-13 中的类声明中看到)。
查看类声明,您可以将内容组合在一起:**
*** 基本必需品:在这里你可以找到构造器和sizeHint
。* 绘画:paintEvent
方法使用变量x
、y
、r
和color
来跟踪要画什么。* 鼠标交互:使用mousePressEvent
、mouseMoveEvent
和mouseReleaseEvent
捕捉鼠标事件。最后已知的鼠标位置保存在mx
和my
中。* 计时:timer
指向的QTimer
对象连接到超时槽。它根据mx
和my
值更新x
、y
、r
和color
。**
注意sizeHint
方法不是必须的,但是我们鼓励你为你所有的小部件实现它。
清单 7-13。 自定义小工具的类声明
class CircleWidget : public QWidget
{
Q_OBJECT
public:
CircleWidget( QWidget *parent=0 );
QSize sizeHint() const;
private slots:
void timeout();
protected:
void paintEvent( QPaintEvent* );
void mousePressEvent( QMouseEvent* );
void mouseMoveEvent( QMouseEvent* );
void mouseReleaseEvent( QMouseEvent* );
private:
int x, y, r;
QColor color;
int mx, my;
QTimer timer;
};
清单 7-14 中所示的构造器将当前圆的半径r
初始化为0
,表示没有圆。然后它配置并连接一个QTimer
对象。计时器间隔设置为 50 毫秒,这意味着圆圈每秒将更新大约 20 次(这通常足以模仿连续的运动)。
清单 7-14。 初始化自定义小工具
CircleWidget::CircleWidget( QWidget *parent ) : QWidget( parent )
{
r = 0;
timer.setInterval( 50 );
connect( &timer, SIGNAL(timeout()), this, SLOT(timeout()) );
}
sizeHint
方法是整个类中最简单的一个;它只是返回一个静态大小(见清单 7-15 )。
清单 7-15。 返回静态大小
QSize CircleWidget::sizeHint() const
{
return QSize( 200, 200 );
}
清单 7-16 展示了跟踪鼠标活动的三种方法。在仔细研究这些方法之前,重要的是要知道只有在按下鼠标按钮时才会报告鼠标移动。这意味着除非按下鼠标按钮,否则不会调用mouseMoveEvent
。
提示将mouseTracking
属性设置为true
可以得到鼠标移动报告。
mousePressEvent
和mouseMoveEvent
都根据传递给QMouseEvent
对象的坐标更新mx
和my
变量。它们由timeout
槽在决定是扩大还是缩小当前圆时使用。timeout
槽与timer
相连,因此可以通过启动和停止mousePressEvent
和mouseReleaseEvent
中的timer
来打开和关闭timeout
槽。仅当按下鼠标按钮时,计时器才会激活(在此期间,mx
和my
值有效)。
清单 7-16。 处理鼠标事件
void CircleWidget::mousePressEvent( QMouseEvent *e )
{
mx = e->x();
my = e->y();
timer.start();
}
void CircleWidget::mouseMoveEvent( QMouseEvent *e )
{
mx = e->x();
my = e->y();
}
void CircleWidget::mouseReleaseEvent( QMouseEvent *e )
{
timer.stop();
}
当定时器激活时,timeout
槽每秒钟被调用大约 20 次。插槽的任务是确定它是否会创建一个新的圆,增长当前的圆,或者缩小它。清单 7-17 展示了它是如何完成的。
如果当前半径r
为0
,则创建一个新的圆,其圆心(x
、y
)在当前鼠标位置:mx
、my
。新的颜色是随机产生的,所以每个新的圆圈都会有新的颜色。
无论是否在新的圆上工作,插槽然后通过使用勾股定理(比较mx
、my
和x
、y
之间的平方距离与半径、r
的平方)来检查mx
、my
是否在圆内。如果鼠标在现有的圆内,半径增加;如果在外面,半径减小。
当对圆的所有更改完成后,将调用 update 方法,该方法将一个 paint 事件放在 Qt 事件队列中。当到达该事件时,调用paintEvent
方法。
清单 7-17。 根据当前圆的位置和大小以及鼠标指针的位置改变圆
void CircleWidget::timeout()
{
if( r == 0 )
{
x = mx;
y = my;
color = QColor( qrand()%256, qrand()%256, qrand()%256 );
}
int dx = mx-x;
int dy = my-y;
if( dx*dx+dy*dy <= r*r )
r++;
else
r--;
update();
}
清单 7-18 中的显示了paintEvent
方法。该方法所做的只是绘制当前圆(如果r
大于 0,则由x
、y
、r
和color
定义)。因为圆的边缘有时看起来有锯齿的趋势,所以你也告诉画家用抗锯齿来柔化边缘(通过设置渲染提示)。顾名思义,它是一个提示,而不是一个有保证的操作。
提示 抗锯齿意味着形状的边缘被平滑。边缘有时会出现锯齿,因为边缘位于可用像素之间。通过计算要添加到每个像素的颜色量,可以获得更平滑的结果(取决于每个像素离边缘有多近)。
简单地绘制新的圆圈而不擦除任何东西是可行的,因为默认情况下 Qt 总是复制背景图形。因为这个小部件不打算放在其他小部件的上面,所以通常意味着纯灰色。通过将autoFillBackground
属性设置为true
,可以强制 Qt 用样式的背景色填充背景。
清单 7-18。 画圆
void CircleWidget::paintEvent( QPaintEvent* )
{
if( r > 0 )
{
QPainter painter( this );
painter.setRenderHint( QPainter::Antialiasing );
painter.setPen( color );
painter.setBrush( color );
painter.drawEllipse( x-r, y-r, 2*r, 2*r );
}
}
在讨论绘画事件时,有几个小部件属性是您应该知道的——它们可以用来进一步优化小部件绘画。您可以使用setAttribute(Qt::WidgetAttribute, bool)
方法设置这些属性。布尔参数,默认情况下是true
,表示应该设置该属性。如果通过了false
,属性被清除。您可以通过使用testAttribute(Qt::WidgetAttribute)
方法来测试一个属性是否被设置。这个不完整的列表解释了一些可用于优化小部件绘制的属性:
- 当小工具重新绘制自己时,它使用不透明的颜色绘制所有的像素。这意味着没有阿尔法混合,Qt 不需要处理背景清除。
Qt::WA_NoSystemBackground
:同Qt::WA_OpaquePaintEvent
,但更明确。没有系统背景的小部件不会被 Qt 事件初始化,所以底层图形会一直亮着,直到小部件被绘制出来。Qt::WA_StaticContents
:内容是静态的,其原点中心在左上角。当这样的小部件被放大时,只有出现在右边和下面的新矩形需要重画。收缩时,根本不需要paintEvent
。
图形视图
到目前为止,你已经通过paintEvent
管理了所有的自定义绘画。图形视图框架考虑到大多数应用程序都是围绕二维画布构建的。通过提供以优化方式处理这种场景的类,可以创建一种定制小部件的感觉,而无需实际创建一个定制小部件。
图形视图框架由三个基本组件构建而成:视图*、场景和项目。视图类QGraphicsView
是一个显示场景内容的小部件。场景QGraphicsScene
包含一组小部件,并管理与这些部件相关的事件和状态的传播。每个条目都是QGraphicsItem
的子类,代表一个单独的图形条目或一组条目。*
*基本的想法是,你创建一组项目,把它放在一个场景中,让一个视图显示它。通过侦听事件和重绘项目,您可以创建所需的用户界面。为了避免创建一组项目,Qt 附带了一系列准备好的项目。
清单 7-19 显示了一个主函数,其中一个场景用标准项目填充,并用一个视图显示。让我们从函数的顶部开始,向下进行。
首先创建一个名为scene
的QGraphicsScene
对象,并将一个QRect
传递给构造器。这个矩形用来定义场景。所有项目都应该出现在这个区域内。请注意,场景可以从非零坐标开始,甚至可以从负坐标开始。
下一步是用物品填充场景。从创建QGraphicsRectItem (QRect,QGraphicsItem*,QGraphicsScene*)
开始。构造器接受一个定义该项目的尺寸和位置的矩形,一个指向父项的QGraphicsItem
指针和一个指向父场景的QGraphicsScene
指针。使用父项目,可以将项目放置在其他项目中(稍后您将了解更多)。通过传递一个场景指针,可以将该项目添加到给定的场景中。您也可以使用从scene
对象中获得的addItem(QGraphicsItem*)
方法来完成这项工作。当矩形被添加到场景中时,你也为它设置了钢笔和画笔。
注意如果你不设置钢笔或画笔,你会以标准设置结束,这通常意味着没有画笔和黑色实线。
您创建的下一个项目是一个QGraphicsSimpleTextItem
。构造器接受一个QString
文本和两个父指针。因为构造器不允许您定位文本,所以调用setPos
方法来定位该项的左上角。
添加一个带有构造器的QGraphicsEllipseItem
,该构造器接受一个矩形和父指针。接下来是一个接受一个QPolygonF
对象和父指针的QGraphicsPolygonItem
。使用QPointF
对象的向量来初始化QPolygonF
。这些点定义了在其间绘制多边形边的点。为这两个对象设置钢笔和画笔。
当这些项目被添加到场景中时,创建一个QGraphicsView
小部件并调用setScene(QGraphicsScene*)
来告诉它显示哪个场景。然后显示视图并运行app.exec()
来启动事件循环。产生的窗口如图 7-28 中的所示。
清单 7-19。 用标准形状填充场景
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QGraphicsScene scene( QRect( −50, −50, 400, 200 ) );
QGraphicsRectItem *rectItem = new QGraphicsRectItem(
QRect( −25, 25, 200, 40 ), 0, &scene );
rectItem->setPen( QPen( Qt::red, 3, Qt::DashDotLine ) );
rectItem->setBrush( Qt::gray );
QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(
"Foundations of Qt", 0, &scene );
textItem->setPos( 50, 0 );
QGraphicsEllipseItem *ellipseItem = new QGraphicsEllipseItem(
QRect( 170, 20, 100, 75 ),
0, &scene );
ellipseItem->setPen( QPen(Qt::darkBlue) );
ellipseItem->setBrush( Qt::blue );
QVector<QPointF> points;
points << QPointF( 10, 10 ) << QPointF( 0, 90 ) << QPointF( 40, 70 )
<< QPointF( 80, 110 ) << QPointF( 70, 20 );
QGraphicsPolygonItem *polygonItem = new QGraphicsPolygonItem(
QPolygonF( points() ), 0, &scene );
polygonItem->setPen( QPen(Qt::darkGreen) );
polygonItem->setBrush( Qt::yellow );
QGraphicsView view;
view.setScene( &scene );
view.show();
return app.exec();
}
图 7-28。 带有一些标准项目的图形视图
图 7-28 和清单 7-19 展示了一些有趣的事情:
- 视图的左上角对应于场景坐标 50,50,因为
QRect
传递给了场景的构造器。 - 矩形项目被多边形和椭圆遮挡,因为场景项目是按照它们被添加到场景中的顺序绘制的。如果不喜欢可以编程控制。
- 如果您尝试自己运行该示例并缩小包含该视图的窗口,该视图将自动显示滑块,让您可以平移整个场景。
Qt 还附带了其他标准项目,下面列出了其中一些:
QGraphicsPathItem
:绘制一条画家路径。QGraphicsLineItem
:画一条单线。QGraphicsPixmapItem
:绘制一个点阵图;即位图图像。QGraphicsSvgtIem
:绘制矢量图形图像。QGraphicsTextItem
:绘制复杂文本,如富文本文档。
您可以使用图形视图自由变换形状项目,图形视图也是项目的父项进入图片的地方。如果一个项的父项被转换,子项也会以同样的方式被转换。
清单 7-20 显示了函数createItem
,它以一个父场景指针和一个 x 偏移量作为参数。然后,这两个参数用于创建一个包含另一个矩形和一个椭圆的矩形。外部矩形用灰色画笔填充;内部项目用白色填充。
该函数返回一个指向外部矩形的指针,而外部矩形又包含另外两个。这意味着指针可以用来操纵所有的形状。
清单 7-20。 一个形状包含另外两个形状
`QGraphicsItem *createItem( int x, QGraphicsScene *scene )
{
QGraphicsRectItem *rectItem = new QGraphicsRectItem(
QRect( x+40, 40, 120, 120 ),
0, scene );
rectItem->setPen( QPen(Qt::black) );
rectItem->setBrush( Qt::gray );
QGraphicsRectItem *innerRectItem = new QGraphicsRectItem(
QRect( x+50, 50, 45, 100 ),
rectItem, scene );
innerRectItem->setPen( QPen(Qt::black) );
innerRectItem->setBrush( Qt::white );
QGraphicsEllipseItem *ellipseItem = new QGraphicsEllipseItem(
QRect( x+105, 50, 45, 100 ),
rectItem, scene );
ellipseItem->setPen( QPen(Qt::black) );
ellipseItem->setBrush( Qt::white );
return rectItem;
}`
在清单 7-21 所示的main
函数中使用了createItem
函数,其中创建了一个场景。然后,在场景显示之前,向该场景添加五个项目。每个项目都以不同的方式进行转换。由此产生的场景可以在图 7-29 中看到。当您查看应用于每个项目的转换时,请参考图和源代码。
图 7-29。 左起:原始、旋转、缩放、剪切,一次全部
item1
项目被放置在场景中,没有应用任何变换。可以看做参考项。
将item2
项平移、旋转 30 度,然后平移回其原始位置,使其围绕(0,0)点旋转。通过平移项目,使其中心点位于点(0,0)上,您可以在将其平移回原始位置之前,围绕其中心旋转它。
item3
项目也被平移,使得点(0,0)成为项目的中心。因为缩放也是相对于坐标系的中心点,所以在平移回来之前会先进行缩放。通过围绕其中心缩放项目,您可以更改形状的大小,但不能更改其位置。
第四项,item4
,翻译和复译为item2
和item3
。在平移之间,它被剪切。
第五项,item5
,被缩放,旋转,剪切,使其变形。这个项目展示了如何将所有的变换应用到一个对象上。
注意应用变换时,记住顺序很重要。以不同的顺序应用变换将产生不同的结果。
清单 7-21。 改造五项
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QGraphicsScene scene( QRect( 0, 00, 1000, 200 ) );
QGraphicsItem *item1 = createItem( 0, &scene );
QGraphicsItem *item2 = createItem( 200, &scene );
item2->translate( 300, 100 );
item2->rotate( 30 );
item2->translate( −300, −100 );
QGraphicsItem *item3 = createItem( 400, &scene );
item3->translate( 500, 100 );
item3->scale( 0.5, 0.7 );
item3->translate( −500, −100 );
QGraphicsItem *item4 = createItem( 600, &scene );
item4->translate( 700, 100 );
item4->shear( 0.1, 0.3 );
item4->translate( −700, −100 );
QGraphicsItem *item5 = createItem( 800, &scene );
item5->translate( 900, 100 );
item5->scale( 0.5, 0.7 );
item5->rotate( 30 );
item5->shear( 0.1, 0.3 );
item5->translate( −900, −100 );
QGraphicsView view;
view.setScene( &scene );
view.show();
return app.exec();
}
处理图形项目时,可以使用 Z 值来控制项目的绘制顺序。您可以使用setZValue(qreal)
方法设置每个项目。任何项目的默认 Z 值都是0
。
绘制场景时,Z 值较高的项目会出现在 Z 值较低的项目之前。对于具有相同 Z 值的项目,顺序是未定义的。
使用自定义项目进行交互
对于自定义项目,您可以使用图形视图创建您想要的行为类型。这种实现自定义形状的灵活性和简易性使得 graphics view 成为一个非常好用的工具。
本节的目的是创建一组手柄:一个中心手柄用于移动形状,两个边缘手柄用于调整形状大小。图 7-30 显示了操作中的手柄。请注意,您可以一次将手柄应用于几个形状,并且使用的形状是标准形状:QGraphicsRectItem
和QGraphicsEllipseItem
。
图 7-30。 手柄在动作
让我们开始看代码,从应用程序的main
函数开始。这展示了如何创建、配置和使用句柄。main
函数如清单 7-22 中的所示。
该函数首先创建您需要的 Qt 类:一个QApplication
、一个QGraphicsScene
,以及通过一个QGraphicsRectItem
和一个QGraphicsEllipseItem
表示的两个形状。当这些形状被添加到场景中后,是时候创建六个HandleItem
对象了——每个形状三个。
每个句柄的构造器接受以下参数:一个要操作的项目、一个场景、一种颜色和一个角色。可用的角色有TopHandle
、RightHandle
和CenterHandle
。当你创建一个CenterHandle
时,你必须传递一个指向另外两个句柄的QList
。也就是说,如果你选择其他手柄,CenterHandle
本身就能完美工作,其他两种型号也是如此。
然后,main
函数继续创建一个QGraphicsView
并设置它来显示场景。然后通过调用QApplication
对象上的exec
方法开始主循环。但是,您不能直接返回结果。因为手柄引用其他形状而不是子节点,所以首先删除手柄很重要。当QGraphicsScene
被销毁时,剩下的图形也被删除。
清单 7-22。 使用 HandleItem
类中的一个场景
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QGraphicsScene scene( 0, 0, 200, 200 );
QGraphicsRectItem *rectItem = new QGraphicsRectItem(
QRect( 10, 10, 50, 100 ),
0, &scene );
QGraphicsEllipseItem *elItem = new QGraphicsEllipseItem(
QRect( 80, 40, 100, 80 ),
0, &scene );
HandleItem *trh = new HandleItem( rectItem, &scene, Qt::red,
HandleItem::TopHandle );
HandleItem *rrh = new HandleItem( rectItem, &scene, Qt::red,
HandleItem::RightHandle );
HandleItem *crh = new HandleItem( rectItem, &scene, Qt::red,
HandleItem::CenterHandle,
QList<HandleItem*>() << trh << rrh );
HandleItem *teh = new HandleItem( elItem, &scene, Qt::green,
HandleItem::TopHandle );
HandleItem *reh = new HandleItem( elItem, &scene, Qt::green,
HandleItem::RightHandle );
HandleItem *ceh = new HandleItem( elItem, &scene, Qt::green,
HandleItem::CenterHandle,
QList<HandleItem*>() << teh << reh );
QGraphicsView view;
view.setScene( &scene );
view.show();
return app.exec();
}
既然您已经知道了句柄的外观以及该类在场景中的用法,那么是时候看看实际的类了。清单 7-23 显示了类声明。
清单以该类的前向声明开始,因为该类将包含指向其自身实例的指针。然后,它定义了不同可用角色的枚举:CenterHandle
、RightHandle
和TopHandle
。
如前所述,enum
后面的构造器包含所有预期的参数。但是,角色和句柄列表有默认值。默认角色是中心句柄,默认情况下列表为空。
从QGraphicsItem
继承时需要下面两个方法。paint 方法负责根据请求绘制形状,而boundingRect
告诉场景形状有多大。
然后,类声明继续一组受保护的方法。您可以重写这些方法,通过形状与用户进行交互。mousePressEvent
和mouseReleaseEvent
方法对鼠标按钮作出反应,而itemChange
方法可用于过滤并对项目的所有更改作出反应。您可以使用它来响应和限制小部件的移动。
私有部分结束类声明。它包含所有需要的本地状态和变量。下面的列表总结了它们的角色和用途(在本节的其余部分,您将更仔细地了解它们的用法):
m_item
:手柄作用的QGraphicsItem
。m_role
:手柄的作用。m_color
:手柄的颜色。m_handles
:作用在同一个m_item
上的其他手柄列表——需要中心手柄。m_pressed
:一个布尔值,表示鼠标按钮是否被按下。这一点很重要,因为您需要能够判断句柄是否因为用户交互或编程更改而移动。
清单 7-23。 手柄类
class HandleItem;
class HandleItem : public QGraphicsItem
{
public:
enum HandleRole
{
CenterHandle,
RightHandle,
TopHandle
};
HandleItem( QGraphicsItem *item, QGraphicsScene *scene,
QColor color, HandleRole role = CenterHandle,
QList<HandleItem*> handles = QList<HandleItem*>() );
void paint( QPainter *paint,
const QStyleOptionGraphicsItem *option, QWidget *widget );
QRectF boundingRect() const;
protected:
void mousePressEvent( QGraphicsSceneMouseEvent *event );
void mouseReleaseEvent( QGraphicsSceneMouseEvent *event );
QVariant itemChange( GraphicsItemChange change, const QVariant &data );
private:
QGraphicsItem *m_item;
HandleRole m_role;
QColor m_color;
QList<HandleItem*> m_handles;
bool m_pressed;
};
在清单 7-24 中显示的构造器在设置一个高zValue
之前简单地初始化所有的类变量。这确保了手柄出现在它们所处理的形状的前面。然后设置一个标志,通过使用setFlag
方法使形状可移动。
提示其他标志允许你选择形状(ItemIsSelectable
)或者接受键盘焦点(ItemIsFocusable
)。这些标志可以通过逻辑或运算来组合。
清单 7-24。 句柄项的构造者
HandleItem::HandleItem( QGraphicsItem *item, QGraphicsScene *scene,
QColor color, HandleItem::HandleRole role,
QList<HandleItem*> handles )
: QGraphicsItem( 0, scene )
{
m_role = role;
m_color = color;
m_item = item;
m_handles = handles;
m_pressed = false;
setZValue( 100 );
setFlag( ItemIsMovable );
}
因为这个类实际上实现了三个不同的句柄,它经常使用switch
语句来区分不同的角色(参见清单 7-25 ,其中展示了boundingRect
方法)。边框由所处理形状的边框位置定义。手柄没有自己的位置;相反,它们完全基于处理形状的位置和大小。
清单 7-25。 确定手柄的外接矩形
QRectF HandleItem::boundingRect() const
{
QPointF point = m_item->boundingRect().center();
switch( m_role )
{
case CenterHandle:
return QRectF( point-QPointF(5, 5), QSize( 10, 10 ) );
case RightHandle:
point.setX( m_item->boundingRect().right() );
return QRectF( point-QPointF(3, 5), QSize( 6, 10 ) );
case TopHandle:
point.setY( m_item->boundingRect().top() );
return QRectF( point-QPointF(5, 3), QSize( 10, 6 ) );
}
return QRectF();
}
清单 7-26 中的所示的绘制方法使用boundingRect
方法来决定在哪里以及如何绘制不同的手柄。中央手柄绘制为圆形,而顶部和右侧手柄绘制为指向上方和右侧的箭头。
注意在绘制顶部和右侧手柄时,使用center
方法找到外接矩形的中心点。
清单 7-26。 画手柄
void HandleItem::paint( QPainter *paint,
const QStyleOptionGraphicsItem *option,
QWidget *widget )
{
paint->setPen( m_color );
paint->setBrush( m_color );
QRectF rect = boundingRect();
QVector<QPointF> points;
switch( m_role )
{
case CenterHandle:
paint->drawEllipse( rect );
break;
case RightHandle:
points << rect.center()+QPointF(3,0) << rect.center()+QPointF(-3,-5)
<< rect.center()+QPointF(-3,5);
paint->drawConvexPolygon( QPolygonF(points) );
break;
case TopHandle:
points << rect.center()+QPointF(0,-3) << rect.center()+QPointF(-5,3)
<< rect.center()+QPointF(5,3);
paint->drawConvexPolygon( QPolygonF(points) );
break;
}
}
在您确定了在哪里绘制,然后绘制手柄之后,下一步是等待用户交互。清单 7-27 显示了处理鼠标按钮事件的方法,比如按下和释放。
因为您在构造器中较早地设置了ItemIsMoveable
标志,所以您所要做的就是在将事件传递给QGraphicsItem
处理程序之前更新m_pressed
变量。
清单 7-27。 处理鼠标按下和释放事件
void HandleItem::mousePressEvent( QGraphicsSceneMouseEvent *event )
{
m_pressed = true;
QGraphicsItem::mousePressEvent( event );
}
void HandleItem::mouseReleaseEvent( QGraphicsSceneMouseEvent *event )
{
m_pressed = false;
QGraphicsItem::mouseReleaseEvent( event );
}
当用户选择移动一个句柄时,itemChange
方法被调用。这种方法给你一个机会来对变化做出反应(或者甚至停止变化)(你可以在清单 7-28 中看到实现)。我删除了清单中处理不同角色移动的部分(稍后您将看到它们);清单只显示了外部框架。简单地让程序性的移动和与移动无关的改变传递给相应的QGraphicsItem
方法。如果遇到用户调用的位置更改,您会根据句柄的角色采取不同的操作。但是首先通过比较新位置和当前位置来计算实际移动。新位置通过data
参数传递,而当前位置由pos
方法给出。您还可以确定正在处理的形状的中心点,因为在处理右侧和顶部手柄时都会用到它。
清单 7-28。 处理对手柄的修改
QVariant HandleItem::itemChange( GraphicsItemChange change,
const QVariant &data )
{
if( change == ItemPositionChange && m_pressed )
{
QPointF movement = data.toPoint() - pos();
QPointF center = m_item->boundingRect().center();
switch( m_role )
{
...
}
}
return QGraphicsItem::itemChange( change, data );
}
清单 7-29 展示了如何处理用户调用的中心手柄的位置变化。通过使用moveBy
调用来移动正在处理的项目m_item
。在m_handles
列表中的所有手柄都被转换到适当的位置,因为任何右侧和顶部的手柄都必须遵循它们正在处理的形状。
清单 7-29。 中心手柄的手柄动作
switch( m_role )
{
case CenterHandle:
m_item->moveBy( movement.x(), movement.y() );
foreach( HandleItem *handle, m_handles )
handle->translate( movement.x(), movement.y() );
break;
...
}
return QGraphicsItem::itemChange( change, pos()+movement );
顶部和右侧的手柄只影响它们自己,这意味着它们不使用m_handles
列表。形状的中心点不受影响;水平方向不受顶部句柄的影响,垂直方向也不受右侧句柄的影响。
清单 7-30 和 7-31 展示了角色是如何被处理的。清单看起来非常相似;唯一的区别是他们行动的方向。
我们来看看清单 7-30 的详细内容;也就是顶把。清单以一个if
子句开始,确保形状不会太小。如果是这种情况,将当前位置作为下一个位置传递给QGraphicsItem itemChange
方法。
如果手柄形状足够大,继续限制手柄的移动方向(不允许顶部手柄水平移动)。然后,您平移正在处理的图形,使图形的中心成为坐标系的原点。这是缩放的准备工作,根据运动来缩放形状。该形状被转换回其原始位置,switch 语句被留下,而QGraphicsItemitemChange
方法被赋予该事件,但是具有受限的移动方向。
清单 7-30。 顶部手柄的操作动作
switch( m_role )
{
...
case TopHandle:
if( −2*movement.y() + m_item->sceneBoundingRect().height() <= 5 )
return QGraphicsItem::itemChange( change, pos() );
movement.setX( 0 );
m_item->translate( center.x(), center.y() );
m_item->scale( 1, 1.0-2.0*movement.y()
/(m_item->sceneBoundingRect().height()) );
m_item->translate( -center.x(), -center.y() );
break;
}
return QGraphicsItem::itemChange( change, pos()+movement );
清单 7-31。 右手柄的操作动作
` switch( m_role )
{
…
case RightHandle:
if( 2*movement.x() + m_item->sceneBoundingRect().width() <= 5 )
return QGraphicsItem::itemChange( change, pos() );
movement.setY( 0 );
m_item->translate( center.x(), center.y() );
m_item->scale( 1.0+2.0*movement.x()
/(m_item->sceneBoundingRect().width()), 1 );
m_item->translate( -center.x(), -center.y() );
break;
…
}
return QGraphicsItem::itemChange ( change, pos()+movement );`
印刷
Qt 用QPrinter
类处理打印机,它代表一个特定打印机的打印作业,可以用作一个绘画设备。这意味着您可以创建一个QPainter
来绘制到通过QPrinter
表示的页面上。然后,printer 对象用于创建新页面,并告知打印机何时可以打印作业。
看看该类中的一些可用属性:
colorMode
:打印机以彩色或灰度打印。可以设置为QPrinter::Color
或QPrinter::GrayScale
。orientation
:页面可以定位为横向(QPrinter::Landscape
)或者纵向(QPrinter::Portrait
)。outputFormat
:打印机可以打印到平台自带的打印系统(QPrinter::Native
)、PDF 文档(QPrinter::PdfFormat
)或 PostScript 文档(QPrinter::PostScriptFormat
)。当打印到文件时,这在创建 PDF 和 PostScript 文档时是必需的,您必须使用setOutputFileName
设置文档的文件名。pageSize
:根据不同标准的纸张大小。包括 A4 (QPrinter::A4
)和 Letter (QPrinter::Letter
)纸张尺寸,但支持更多尺寸。详情请参考 Qt 文档。
让我们继续进行一些实际的打印。
在尝试打印时,拥有一个虚拟打印机驱动程序或打印到一个文件会非常有用——它可以节省大量纸张。
画到打印机上
向打印机绘图最直接的方法是创建一个QPainter
来直接访问QPrinter
对象。要配置QPrinter
对象,使用QPrintDialog
标准对话框(见图 7-31 ),用户可以在其中选择打印机,也可以对打印作业进行一些基本选择。
图 7-31。 出现打印机选择和配置对话框
清单 7-32 显示了创建五页打印输出的整个应用程序的源代码。打印作业中某一页的顶部如图 7-32 所示。
图 7-32。 一★页
清单 7-32 从创建QApplication
、QPrinter
和QPrintDialog
开始。然后执行该对话;如果它被接受,你将做一些印刷。
当您创建一个引用打印机对象的QPainter
并将其设置为使用黑色钢笔时,实际的打印就准备好了。然后使用一个for
循环来创建五个页面。对于每一页,在QPrinterpageRect
中画一个矩形和两条交叉的线。这是一个代表可打印区域的矩形(代表整张纸的矩形称为paperRect
)。
计算textArea
矩形的尺寸。(这个矩形的两边和顶部有半英寸的边距,底部有整整一英寸的边距。)分辨率方法给出每英寸的点数,因此0.5*printer.resolution()
得出覆盖半英寸所需的点数。在文本区域周围画一个框,然后将页码作为文本打印在同一个矩形内。
如果你不在最后一页,也就是说,这一页不等于四,调用newPage
方法。该页面打印当前页面并创建一个新的空白页以继续绘画。
清单 7-32。 画到一个 QPrinter
的物体上
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QPrinter printer;
QPrintDialog dlg( &printer );
if( dlg.exec() == QDialog::Accepted )
{
QPainter painter( &printer );
painter.setPen( Qt::black );
for( int page=0; page<5; page++ )
{
painter.drawRect( printer.pageRect() );x
painter.drawLine( printer.pageRect().topLeft(),
printer.pageRect().bottomRight() );
painter.drawLine( printer.pageRect().topRight(),
printer.pageRect().bottomLeft() );
QRectF textArea(
printer.pageRect().left() +printer.resolution() * 0.5,
printer.pageRect().top() +printer.resolution() * 0.5,
printer.pageRect().width() -printer.resolution() * 1.0,
printer.pageRect().height()-printer.resolution() * 1.5 );
painter.drawRect( textArea );
painter.drawText( textArea, Qt::AlignTop | Qt::AlignLeft,
QString( "Page %1" ).arg( page+1 ) );
if( page != 4 )
printer.newPage();
}
}
return 0;
}
将一个图形场景渲染到打印机上
使用 painter 对象在打印机上绘图可能很容易,但是如果您的整个文档都基于图形视图框架,这就没什么用了。你必须能够将你的场景渲染到打印机上,这很容易做到。
将清单 7-33 与清单 7-19 进行比较。清单 7-33 使用与清单 7-19 相同的场景,但它不是通过场景显示,而是使用render
方法将它打印到打印机。通过比较图 7-33 和图 7-28 可以比较输出。正如你所看到的,这个场景在纸上和屏幕上都表现得很好。
图 7-33。 一个打印出来的图形场景
render
方法接受四个参数。从左到右,它们是要渲染到的画师、目标矩形、源矩形和确定如何缩放的标志。在清单中,画师绘制了一个QPrinter
对象。目标矩形代表页面的整个可打印区域,而源矩形代表整个场景。缩放标志被设置为Qt::KeepAspectRatio
,这意味着场景的高宽比将保持不变。
如果你想让场景拉伸以填充目标矩形,你可以使用Qt::IgnoreAspectRatio
。另一种选择是让场景填充页面,但仍然通过传递Qt::KeepAspectRatioByExpanding
来保持其高宽比。这意味着,除非源矩形和目标矩形具有相同的部分,否则场景将超出可用页面。
清单 7-33。 向打印机渲染一个图形场景
int main( int argc, char **argv )
{
QApplication app( argc, argv );
QGraphicsScene scene( QRect( −50, −50, 400, 200 ) );
...
QPrinter printer;
QPrintDialog dlg( &printer );
if( dlg.exec() )
{
QPainter painter( &printer );
scene.render( &painter, printer.pageRect(),
scene.sceneRect(), Qt::KeepAspectRatio );
}
return 0;
}
OpenGL
在本章的第一段,我提到了使用QPainter
类的唯一替代方法是直接使用 OpenGL。因为 OpenGL 是一种编程接口,不在本书的讨论范围之内,所以您将看到如何在不直接编写 OpenGL 代码的情况下使用 OpenGL 的硬件加速。
一个QGraphicsView
是一个给定场景的视口,但是它也包含一个可以通过viewport
属性访问的视口部件。如果您为视图提供一个QGLWidget
,图形将使用 OpenGL 绘制。
在清单 7-21 中,所需的更改仅限于清单 7-34 中高亮显示的行。代码创建一个新的QGLWidget
并将其设置为 viewport。QGraphicsView
项拥有其视口的所有权,所以不需要提供父指针。
清单 7-34。 使用 OpenGL 绘制图形场景
int main( int argc, char **argv )
{
...
QGraphicsView view;
view.setScene( &scene );
view.setViewport( new QGLWidget() );
view.show();
return app.exec();
}
要使用 OpenGL 构建 Qt 应用程序,您必须通过在项目文件中添加一行内容QT += opengl
来包含 Qt OpenGL 模块。使用 OpenGL 绘制场景和使用普通小部件绘制场景之间的区别是看不出来的——这才是重点。然而,在提供 OpenGL 硬件加速的系统上,性能会大大提高。
总结
使用QPainter
类很容易绘制,它可以用于绘制各种设备(屏幕、图像、位图和打印机)。通过缩放、旋转、剪切和平移,几乎可以画出任何可以想象的形状。
QPainter
类是创建带有绘画逻辑的定制小部件的主力。如果您想在单个文档或小部件中表示多个独立的形状,图形视图框架会很有帮助。通过创建一个QGraphicsScene
并用QGraphicsItem
对象填充它,您可以很容易地为用户创建一个交互式画布。可以使用QGraphicsView
小工具显示场景,也可以使用QPainter
打印到QPrinter
上。*****