进bitman时写的画板,现在写一个笔记回忆下当时的思路。

任务内容

写一个类似于windows画图的画板程序,要求至少提供直线,正方形和圆三种图形,支持拖拽以及文件保存读取。

用到的Qt特性

基于知道啥用啥的原则,选择Qt作为sdk编写。Qt的特性决定了这个程序怎么架构。

信号和槽

属于Qt元对象机制的内容。

信号与槽(Signal & Slot)是 Qt 编程的基础,也是 Qt 的一大创新。因为有了信号与槽的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。

信号(Signal)就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号,一个 ComboBox 最常见的信号是选择的列表项变化时发射的 CurrentIndexChanged() 信号。

槽(Slot)就是对信号响应的函数。槽是一个函数,与一般的 C++ 函数一样,可以定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。

事件机制

键鼠交互和图形绘制所需要。

【1】事件

事件是可以被控件识别的操作。如按下确定按钮、选择某个单选按钮或复选框。

每种控件有自己可识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件等等。

事件就是用户对窗口上各种组件的操作。

【2】Qt事件

由窗口系统或Qt自身产生的,用以响应所发生各类事情的操作。具体点,Qt事件是一个QEvent对象,用于描述程序内部或外部发生的动作。

【3】Qt事件产生类型

1、键盘或鼠标事件:用户按下或松开键盘或鼠标上的按键时,就可以产生一个键盘或者鼠标事件。

2、绘制事件:某个窗口第一次显示的时候,就会产生一个绘制事件,用来告诉窗口需要重新绘制它本身,从而使得该窗口可见。

3、QT事件:Qt自己也会产生很多事件,比如QObject::startTimer()会触发QTimerEvent。

另外还用到了元对象机制的其他内容,如内存管理之类。

基本构想

绘图

设计一个graphcontroller类,包含成员画板QPainter类实例,管理所有的图形和键鼠与画板之间的互动。其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class GraphController : public QWidget
{
Q_OBJECT
public:
explicit GraphController(QWidget *parent = nullptr);

enum Shape {
line = 0, ellipse = 1, rectangle = 2
};
enum Status {
drag, creat, null
};

QList<GraphBase*>* GetList(void);

public slots:
void ChangeShape(GraphController::Shape);

protected:
void paintEvent(QPaintEvent*);

void mousePressEvent(QMouseEvent*);
void mouseReleaseEvent(QMouseEvent*);
void mouseMoveEvent(QMouseEvent*);

void keyPressEvent(QKeyEvent*);
void keyReleaseEvent(QKeyEvent*);

private:
QList<GraphBase*> ShapeList;

int order_stamp;
Shape current_shape;
Status now_sta;
GraphBase* current_graph;
QPoint relative;
bool now_shifted;

};

graphcontroller类中使用链表管理当前已经画上的图形,即储存图形抽象类的指针。

当鼠标按下时,通过事件机制获取当前坐标,此时要判断要进行拖拽还是创建新图形。遍历已画的图形判断即可。

新建图形时,需记录当前选中的形状;拖拽时,找出顺序戳最前的图形进行选中。另外记录shift键是否按下来绘制方形。接收鼠标位移事件时判断当前画板状态并计算相对位移,注意qt窗口的坐标机制。

形状

所有形状继承自抽象类graphbase,重载实现所有功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class GraphBase : public QObject
{
Q_OBJECT
public:
explicit GraphBase(QPoint Spos, QPoint Epos, int order, QObject *parent = nullptr);

int order(void) const;
void move(QPoint, QPoint);
void move(QPoint);
QPoint StartPoint();
QPoint EndPoint();
void fixpos();

virtual void paint(QPainter&) = 0;
virtual bool inrange(QPoint) = 0;
virtual int Shape_type(void) = 0;

protected:
QPoint Spos, Epos;

static bool equal(qreal, qreal, qreal);
static bool between(qreal val, qreal a, qreal b);

private:
int graph_order;

};

每个子类要实现三个功能,分别为调用画板绘制当前图形,判断某个点是否位于当前图形上,返回当前图形形状值(这种做法其实不好)。

文件操作

以特定的文字格式输出到某个文件后方便读取,迭代器遍历graphcontroller类中的链表即可。也可以设计输出到二进制文件中,甚至加密。

主窗口

新建子组件graphcontroller类以及一堆ToolBar,使窗口上面是工具栏下面是画板。工具栏排布保存加载按钮以及图形选择按钮。

实现细节

主窗口与graphcontroller类通讯

改变当前形状的时候触发mainwindow定义的信号,由graphcontroller类接收。

图形坐标

注意基类中记录的图形坐标位于该图形的哪个相对位置,尤其是绘制时和判断点是否在图形上时。

线段

线段判断点是否在图形上时,如果采用斜率判断需要特判竖直的情况;如果采用将式子乘开的绝对误差判断则需要与相应的分母作为基准值比较。

成品效果

总结

不同sdk用的gui机制不同,qt可能算是相对简单的。实际上第一次写完出成品问题挺大的,绘图bug很多,只是勉强能用,所以图形逻辑一定要设计好。

新手入门Qt渣作。源代码请私聊我喵。