-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwidget.cpp
More file actions
1237 lines (1111 loc) · 43.3 KB
/
widget.cpp
File metadata and controls
1237 lines (1111 loc) · 43.3 KB
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* ============================================================================
* 文件名: widget.cpp
* 作用: 主窗口类的具体实现
*
* 学习要点:
* 1. 实现文件包含头文件中声明的所有函数
* 2. 构造函数中进行初始化工作
* 3. 信号槽连接实现界面与逻辑的交互
* 4. 每个函数都有明确的职责
* ============================================================================
*/
#include "widget.h" // 包含类声明
#include "ui_widget.h" // 包含UI类定义(由Qt Designer自动生成)
// 标准库和Qt库
#include <QApplication> // 应用程序类
#include <QFileDialog> // 文件对话框
#include <QTextStream> // 文本流(用于文件读写)
/*
* ============================================================================
* 构造函数实现
*
* 学习要点:
* 1. 初始化列表用于初始化成员变量,效率更高
* 2. 父类构造函数必须首先调用
* 3. 指针成员变量需要用new创建对象
* 4. bool变量设置初始状态
* 5. 构造函数中调用初始化函数,保持代码整洁
* ============================================================================
*/
Widget::Widget(QWidget *parent)
: QWidget(parent) // 调用父类QWidget的构造函数
, ui(new Ui::Widget) // 创建UI对象,连接到.ui文件
, m_serial(new QSerialPort(this)) // 创建串口对象,this作为父对象实现自动内存管理
, m_isConnected(false) // 初始化连接状态为未连接
, m_rxBytes(0) // 初始化接收字节数为0
, m_txBytes(0) // 初始化发送字节数为0
, m_rxHexMode(true) // 初始化接收显示模式为HEX
, m_txHexMode(true) // 初始化发送显示模式为HEX
, m_showTimestamp(true) // 初始化时间戳显示为开启
, m_binarySendMode(false) // 初始化发送模式为ASCII(新增功能)
, m_autoSendTimer(new QTimer(this)) // 创建自动发送定时器
, m_autoSendEnabled(false) // 初始化自动发送为关闭
, m_autoSendInterval(1000) // 初始化自动发送间隔为1000毫秒
{
/*
* UI初始化步骤:
* 1. setupUi() - 根据.ui文件创建界面
* 2. setupUIReferences() - 获取UI控件的引用
* 3. setupConnections() - 连接信号槽
* 4. setupStyles() - 设置界面样式
* 5. initializeSerialSettings() - 初始化串口参数
* 6. updateConnectionStatus() - 设置初始连接状态
*/
ui->setupUi(this); // 根据.ui文件创建界面控件
setupUIReferences(); // 获取UI控件的指针引用
setupConnections(); // 连接信号槽
setupStyles(); // 设置界面样式
initializeSerialSettings(); // 初始化串口参数选项
updateConnectionStatus(false); // 设置初始连接状态为未连接
}
/*
* ============================================================================
* 析构函数实现
*
* 学习要点:
* 1. 析构函数负责清理资源
* 2. 关闭打开的串口连接
* 3. 删除UI对象
* 4. 其他指针对象由Qt的父子关系自动管理
* ============================================================================
*/
Widget::~Widget()
{
// 如果串口还在连接状态,先关闭它
if (m_serial->isOpen()) {
m_serial->close();
}
// 删除UI对象,释放界面资源
delete ui;
// 注意:其他用this作为父对象创建的指针(如m_serial, m_autoSendTimer)
// 会由Qt的父子关系自动删除,不需要手动delete
}
/*
* ============================================================================
* UI控件引用设置函数
*
* 学习要点:
* 1. 这个函数将.ui文件中的控件与成员变量指针关联
* 2. 通过ui->controlName获取控件指针
* 3. 这样做的好处是可以在代码中直接操作界面控件
* 4. 设置窗口的基本属性
* ============================================================================
*/
void Widget::setupUIReferences()
{
/*
* 串口配置控件引用设置
*
* 对应UI设计步骤:
* 1. 在Qt Designer中拖拽QComboBox到serialConfigGroup
* 2. 设置objectName为portComboBox, baudRateComboBox等
* 3. 在这里通过ui->objectName获取控件指针
*/
m_portComboBox = ui->portComboBox; // 串口选择下拉框
m_baudRateComboBox = ui->baudRateComboBox; // 波特率选择下拉框
m_dataBitsComboBox = ui->dataBitsComboBox; // 数据位选择下拉框
m_parityComboBox = ui->parityComboBox; // 校验位选择下拉框
m_stopBitsComboBox = ui->stopBitsComboBox; // 停止位选择下拉框
/*
* 控制按钮引用设置
*
* 对应UI设计步骤:
* 1. 在Qt Designer中拖拽QPushButton到相应位置
* 2. 设置按钮的text属性(显示文本)
* 3. 设置objectName属性(用于代码中引用)
*/
m_connectButton = ui->connectButton; // 连接/断开按钮
m_refreshButton = ui->refreshButton; // 刷新串口列表按钮
m_sendButton = ui->sendButton; // 发送数据按钮
m_clearRxButton = ui->clearRxButton; // 清除接收数据按钮
m_clearTxButton = ui->clearTxButton; // 清除发送数据按钮
m_clearAllButton = ui->clearAllButton; // 清除所有数据按钮
m_saveButton = ui->saveButton; // 保存数据按钮
m_autoSendButton = ui->autoSendButton; // 自动发送按钮
/*
* 选项控件引用设置
*
* 对应UI设计步骤:
* 1. 拖拽QCheckBox到displayOptionsGroup
* 2. 设置checked属性为true(默认选中)
* 3. 拖拽QSpinBox到autoSendGroup,设置范围和默认值
*/
m_rxHexCheckBox = ui->rxHexCheckBox; // 接收HEX显示复选框
m_txHexCheckBox = ui->txHexCheckBox; // 发送HEX显示复选框
m_timestampCheckBox = ui->timestampCheckBox; // 时间戳显示复选框
m_autoSendSpinBox = ui->autoSendSpinBox; // 自动发送间隔输入框
/*
* 发送模式控件引用设置(新增功能)
*
* 对应UI设计步骤:
* 1. 在sendGroup中添加QHBoxLayout
* 2. 拖拽两个QRadioButton到布局中
* 3. 设置第一个按钮的checked属性为true(默认选中ASCII模式)
* 4. 设置按钮的text属性分别为"ASCII"和"二进制(HEX)"
*/
m_asciiSendRadio = ui->asciiSendRadio; // ASCII发送模式单选按钮
m_binarySendRadio = ui->binarySendRadio; // 二进制发送模式单选按钮
/*
* 显示控件引用设置
*
* 对应UI设计步骤:
* 1. 拖拽QTextEdit到dataDisplayGroup作为数据显示区域
* 2. 设置readOnly属性为true(只读)
* 3. 拖拽QLineEdit到sendGroup作为数据输入框
* 4. 设置placeholderText属性(提示文本)
*/
m_displayArea = ui->displayArea; // 数据显示区域
m_sendLineEdit = ui->sendLineEdit; // 发送数据输入框
/*
* 统计标签引用设置
*
* 对应UI设计步骤:
* 1. 拖拽QLabel到statisticsGroup
* 2. 设置初始text属性为"接收: 0 字节"等
*/
m_rxBytesLabel = ui->rxBytesLabel; // 接收字节数显示标签
m_txBytesLabel = ui->txBytesLabel; // 发送字节数显示标签
/*
* 状态标签设置
*
* 学习要点:
* - 由于我们没有在UI文件中创建状态栏,所以设置为nullptr
* - 这样可以避免空指针访问错误
*/
m_statusLabel = nullptr; // 状态标签(未使用状态栏)
/*
* 窗口属性设置
*
* 学习要点:
* - 设置窗口标题、最小尺寸、默认尺寸
* - 这些属性也可以在.ui文件中设置
*/
setWindowTitle("串口助手"); // 设置窗口标题
setMinimumSize(900, 600); // 设置最小窗口尺寸
resize(1200, 800); // 设置默认窗口尺寸
}
/*
* ============================================================================
* 信号槽连接设置函数
*
* 学习要点:
* 1. 信号槽是Qt的核心机制,用于对象间通信
* 2. connect函数语法:connect(发送者, 信号, 接收者, 槽函数)
* 3. 使用新式信号槽语法(函数指针),编译时检查类型安全
* 4. Lambda表达式可以用于简单的槽函数实现
* 5. 按功能分组组织信号槽连接
* ============================================================================
*/
void Widget::setupConnections()
{
/*
* 串口连接相关信号槽
*
* 学习要点:
* - QPushButton::clicked信号在按钮被点击时发出
* - Lambda表达式 [this]() {...} 用于捕获this指针
* - 三元运算符 condition ? value1 : value2 用于条件判断
*/
// 连接/断开按钮点击事件
// 根据当前连接状态决定是连接还是断开串口
connect(m_connectButton, &QPushButton::clicked, this, [this]() {
if (m_isConnected) {
closeSerial(); // 如果已连接,则断开
} else {
openSerial(); // 如果未连接,则连接
}
});
// 刷新串口列表按钮点击事件
connect(m_refreshButton, &QPushButton::clicked, this, &Widget::refreshSerialList);
/*
* 数据收发相关信号槽
*
* 学习要点:
* - QLineEdit::returnPressed信号在按下回车键时发出
* - QSerialPort::readyRead信号在有数据可读时发出
* - 同一个槽函数可以连接多个信号
*/
// 发送按钮点击事件和输入框回车事件都触发发送数据
connect(m_sendButton, &QPushButton::clicked, this, &Widget::sendData);
connect(m_sendLineEdit, &QLineEdit::returnPressed, this, &Widget::sendData);
// 串口数据接收事件
// 当串口有数据可读时,自动调用readSerialData函数
connect(m_serial, &QSerialPort::readyRead, this, &Widget::readSerialData);
/*
* 清除按钮相关信号槽
*
* 学习要点:
* - 每个清除按钮对应不同的清除功能
* - 函数指针语法:&ClassName::functionName
*/
connect(m_clearRxButton, &QPushButton::clicked, this, &Widget::clearReceiveArea);
connect(m_clearTxButton, &QPushButton::clicked, this, &Widget::clearSendArea);
connect(m_clearAllButton, &QPushButton::clicked, this, &Widget::clearAllData);
connect(m_saveButton, &QPushButton::clicked, this, &Widget::saveDataToFile);
/*
* 显示模式相关信号槽
*
* 学习要点:
* - QCheckBox::toggled信号在复选框状态改变时发出
* - 信号会传递bool参数表示新的选中状态
*/
connect(m_rxHexCheckBox, &QCheckBox::toggled, this, &Widget::toggleReceiveMode);
connect(m_txHexCheckBox, &QCheckBox::toggled, this, &Widget::toggleSendMode);
/*
* 发送模式相关信号槽(新增功能)
*
* 学习要点:
* - QRadioButton::toggled信号在单选按钮状态改变时发出
* - 两个单选按钮都连接到同一个槽函数
* - 在槽函数中通过isChecked()判断哪个按钮被选中
*/
connect(m_asciiSendRadio, &QRadioButton::toggled, this, &Widget::onSendModeChanged);
connect(m_binarySendRadio, &QRadioButton::toggled, this, &Widget::onSendModeChanged);
/*
* 自动发送相关信号槽
*
* 学习要点:
* - QTimer::timeout信号在定时器超时时发出
* - QSpinBox::valueChanged信号在数值改变时发出
* - QOverload用于指定重载函数的具体版本
* - Lambda表达式可以捕获参数并执行简单操作
*/
connect(m_autoSendButton, &QPushButton::clicked, this, &Widget::toggleAutoSend);
connect(m_autoSendTimer, &QTimer::timeout, this, &Widget::autoSendData);
// 自动发送间隔改变事件
// QSpinBox有多个valueChanged重载版本,使用QOverload指定int版本
connect(m_autoSendSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
[this](int value) {
m_autoSendInterval = value; // 更新自动发送间隔
});
}
/*
* ============================================================================
* 界面样式设置函数
*
* 学习要点:
* 1. Qt样式表(StyleSheet)类似于CSS,用于美化界面
* 2. R"(...)" 是C++11的原始字符串字面量,避免转义字符
* 3. 样式表可以设置颜色、字体、边框、圆角等属性
* 4. 选择器语法:控件类型、ID选择器、伪状态等
* 5. 现代化界面设计原则:扁平化、圆角、阴影、配色协调
* ============================================================================
*/
void Widget::setupStyles()
{
/*
* 定义样式表字符串
*
* 学习要点:
* - 使用原始字符串字面量R"(...)"避免转义字符
* - 样式表语法类似CSS
* - 可以为不同控件类型设置不同样式
*/
QString styleSheet = R"(
/*
* 全局控件样式
* 作用于所有QWidget及其子类
*/
QWidget {
background-color: #f5f5f5; /* 浅灰色背景,现代化设计 */
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif; /* 字体族 */
}
/*
* 分组框样式
* 对应UI中的QGroupBox控件
*/
QGroupBox {
font-weight: bold; /* 粗体字 */
border: 2px solid #cccccc; /* 2像素实线边框 */
border-radius: 8px; /* 8像素圆角 */
margin-top: 1ex; /* 顶部外边距 */
padding-top: 10px; /* 顶部内边距 */
background-color: white; /* 白色背景 */
}
/*
* 分组框标题样式
* ::title是QGroupBox的子控件选择器
*/
QGroupBox::title {
subcontrol-origin: margin; /* 标题位置基于外边距 */
left: 10px; /* 左偏移10像素 */
padding: 0 8px 0 8px; /* 左右内边距8像素 */
color: #2c3e50; /* 深蓝灰色文字 */
}
/*
* 按钮基础样式
* 对应UI中的QPushButton控件
*/
QPushButton {
background-color: #3498db; /* 蓝色背景 */
border: none; /* 无边框 */
color: white; /* 白色文字 */
padding: 8px 16px; /* 内边距:上下8px,左右16px */
border-radius: 4px; /* 4像素圆角 */
font-weight: bold; /* 粗体字 */
min-height: 20px; /* 最小高度20像素 */
}
/*
* 按钮悬停状态样式
* :hover是伪状态选择器,鼠标悬停时生效
*/
QPushButton:hover {
background-color: #2980b9; /* 悬停时变为深蓝色 */
}
/*
* 按钮按下状态样式
* :pressed是伪状态选择器,鼠标按下时生效
*/
QPushButton:pressed {
background-color: #21618c; /* 按下时变为更深的蓝色 */
}
/*
* 按钮禁用状态样式
* :disabled是伪状态选择器,按钮禁用时生效
*/
QPushButton:disabled {
background-color: #bdc3c7; /* 禁用时变为灰色 */
}
/*
* 连接按钮特殊样式
* #connectButton是ID选择器,对应objectName为"connectButton"的按钮
*/
QPushButton#connectButton {
background-color: #27ae60; /* 绿色背景(表示连接) */
font-size: 14px; /* 字体大小14像素 */
font-weight: bold; /* 粗体字 */
}
QPushButton#connectButton:hover {
background-color: #229954; /* 悬停时的深绿色 */
}
/*
* 断开按钮特殊样式
* 当连接状态改变时,按钮的objectName会动态改变
*/
QPushButton#disconnectButton {
background-color: #e74c3c; /* 红色背景(表示断开) */
}
QPushButton#disconnectButton:hover {
background-color: #c0392b; /* 悬停时的深红色 */
}
/*
* 下拉框样式
* 对应UI中的QComboBox控件
*/
QComboBox {
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 4px; /* 4像素圆角 */
padding: 4px 8px; /* 内边距:上下4px,左右8px */
background-color: white; /* 白色背景 */
min-height: 20px; /* 最小高度20像素 */
}
/*
* 下拉框获得焦点时的样式
* :focus是伪状态选择器,控件获得焦点时生效
*/
QComboBox:focus {
border-color: #3498db; /* 焦点时边框变为蓝色 */
}
/*
* 下拉框下拉按钮样式
* ::drop-down是QComboBox的子控件选择器
*/
QComboBox::drop-down {
border: none; /* 无边框 */
width: 20px; /* 宽度20像素 */
}
/*
* 下拉框箭头样式
* ::down-arrow是QComboBox的子控件选择器
* 使用CSS三角形技巧创建箭头
*/
QComboBox::down-arrow {
image: none; /* 不使用图片 */
border-left: 5px solid transparent; /* 左边透明边框 */
border-right: 5px solid transparent; /* 右边透明边框 */
border-top: 5px solid #7f8c8d; /* 顶部边框形成箭头 */
margin-right: 5px; /* 右边距5像素 */
}
/*
* 单行文本输入框样式
* 对应UI中的QLineEdit控件
*/
QLineEdit {
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 4px; /* 4像素圆角 */
padding: 8px; /* 内边距8像素 */
background-color: white; /* 白色背景 */
font-size: 12px; /* 字体大小12像素 */
}
QLineEdit:focus {
border-color: #3498db; /* 焦点时边框变为蓝色 */
}
/*
* 多行文本编辑框样式
* 对应UI中的QTextEdit控件(数据显示区域)
*/
QTextEdit {
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 4px; /* 4像素圆角 */
background-color: white; /* 白色背景 */
font-family: "Consolas", "Courier New", monospace; /* 等宽字体 */
font-size: 11px; /* 字体大小11像素 */
}
/*
* 复选框样式
* 对应UI中的QCheckBox控件
*/
QCheckBox {
spacing: 8px; /* 文字与复选框间距8像素 */
color: #2c3e50; /* 深蓝灰色文字 */
}
/*
* 复选框指示器样式
* ::indicator是QCheckBox的子控件选择器
*/
QCheckBox::indicator {
width: 16px; /* 宽度16像素 */
height: 16px; /* 高度16像素 */
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 3px; /* 3像素圆角 */
background-color: white; /* 白色背景 */
}
/*
* 复选框选中状态样式
* :checked是伪状态选择器,复选框选中时生效
*/
QCheckBox::indicator:checked {
background-color: #3498db; /* 选中时蓝色背景 */
border-color: #3498db; /* 选中时蓝色边框 */
}
/*
* 单选按钮样式(新增功能)
* 对应UI中的QRadioButton控件
*/
QRadioButton {
spacing: 8px; /* 文字与单选按钮间距8像素 */
color: #2c3e50; /* 深蓝灰色文字 */
}
/*
* 单选按钮指示器样式
* ::indicator是QRadioButton的子控件选择器
*/
QRadioButton::indicator {
width: 16px; /* 宽度16像素 */
height: 16px; /* 高度16像素 */
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 8px; /* 8像素圆角(圆形) */
background-color: white; /* 白色背景 */
}
/*
* 单选按钮选中状态样式
* :checked是伪状态选择器,单选按钮选中时生效
*/
QRadioButton::indicator:checked {
background-color: #3498db; /* 选中时蓝色背景 */
border-color: #3498db; /* 选中时蓝色边框 */
}
/*
* 单选按钮选中后的内部圆点
* ::after伪元素用于在选中状态下显示内部圆点
*/
QRadioButton::indicator:checked::after {
content: ""; /* 空内容 */
width: 6px; /* 宽度6像素 */
height: 6px; /* 高度6像素 */
border-radius: 3px; /* 3像素圆角(圆形) */
background-color: white; /* 白色背景 */
margin: 3px; /* 外边距3像素 */
}
/*
* 数字输入框样式
* 对应UI中的QSpinBox控件
*/
QSpinBox {
border: 2px solid #bdc3c7; /* 2像素实线边框 */
border-radius: 4px; /* 4像素圆角 */
padding: 4px; /* 内边距4像素 */
background-color: white; /* 白色背景 */
min-height: 20px; /* 最小高度20像素 */
}
QSpinBox:focus {
border-color: #3498db; /* 焦点时边框变为蓝色 */
}
/*
* 框架控件样式
* 对应UI中的QFrame控件
*/
QFrame {
background-color: transparent; /* 透明背景 */
}
/*
* 标签样式
* 对应UI中的QLabel控件
*/
QLabel {
color: #2c3e50; /* 深蓝灰色文字 */
}
)";
/*
* 应用样式表到整个窗口
*
* 学习要点:
* - setStyleSheet()方法将样式表应用到控件及其所有子控件
* - 样式表会覆盖默认的系统样式
*/
setStyleSheet(styleSheet);
/*
* 设置连接按钮的特殊对象名称
*
* 学习要点:
* - setObjectName()设置控件的对象名称
* - 对象名称可以在样式表中用作ID选择器
* - 这样可以为特定控件设置特殊样式
*/
m_connectButton->setObjectName("connectButton");
}
void Widget::initializeSerialSettings()
{
// 刷新串口列表
refreshSerialList();
// 初始化波特率选项
QStringList baudRates = {"1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"};
m_baudRateComboBox->addItems(baudRates);
m_baudRateComboBox->setCurrentText("115200");
// 初始化数据位选项
QStringList dataBits = {"5", "6", "7", "8"};
m_dataBitsComboBox->addItems(dataBits);
m_dataBitsComboBox->setCurrentText("8");
// 初始化校验位选项
m_parityComboBox->addItem("无校验", QSerialPort::NoParity);
m_parityComboBox->addItem("偶校验", QSerialPort::EvenParity);
m_parityComboBox->addItem("奇校验", QSerialPort::OddParity);
m_parityComboBox->addItem("标记校验", QSerialPort::MarkParity);
m_parityComboBox->addItem("空格校验", QSerialPort::SpaceParity);
// 初始化停止位选项
m_stopBitsComboBox->addItem("1", QSerialPort::OneStop);
m_stopBitsComboBox->addItem("1.5", QSerialPort::OneAndHalfStop);
m_stopBitsComboBox->addItem("2", QSerialPort::TwoStop);
}
void Widget::refreshSerialList()
{
m_portComboBox->clear();
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &port : ports) {
QString portInfo = QString("%1 (%2)").arg(port.portName(), port.description());
m_portComboBox->addItem(portInfo, port.portName());
}
if (m_portComboBox->count() == 0) {
m_portComboBox->addItem("无可用串口");
m_connectButton->setEnabled(false);
} else {
m_connectButton->setEnabled(true);
}
}
void Widget::openSerial()
{
if (m_portComboBox->count() == 0 || m_portComboBox->currentData().toString().isEmpty()) {
QMessageBox::warning(this, "错误", "请选择有效的串口!");
return;
}
QString portName = m_portComboBox->currentData().toString();
m_serial->setPortName(portName);
// 设置串口参数
bool ok;
qint32 baudRate = m_baudRateComboBox->currentText().toInt(&ok);
if (!ok) baudRate = 115200;
m_serial->setBaudRate(baudRate);
QSerialPort::DataBits dataBits = static_cast<QSerialPort::DataBits>(m_dataBitsComboBox->currentText().toInt());
m_serial->setDataBits(dataBits);
QSerialPort::Parity parity = static_cast<QSerialPort::Parity>(m_parityComboBox->currentData().toInt());
m_serial->setParity(parity);
QSerialPort::StopBits stopBits = static_cast<QSerialPort::StopBits>(m_stopBitsComboBox->currentData().toInt());
m_serial->setStopBits(stopBits);
m_serial->setFlowControl(QSerialPort::NoFlowControl);
if (m_serial->open(QIODevice::ReadWrite)) {
updateConnectionStatus(true);
// 重置统计
m_rxBytes = 0;
m_txBytes = 0;
updateStatistics();
appendToDisplay("=== 串口连接成功 ===", "#27ae60");
} else {
QMessageBox::critical(this, "连接失败",
QString("无法打开串口 %1\n错误: %2")
.arg(portName)
.arg(m_serial->errorString()));
}
}
void Widget::closeSerial()
{
if (m_serial->isOpen()) {
m_serial->close();
}
// 停止自动发送
if (m_autoSendEnabled) {
toggleAutoSend();
}
updateConnectionStatus(false);
appendToDisplay("=== 串口连接已断开 ===", "#e74c3c");
}
void Widget::updateConnectionStatus(bool connected)
{
m_isConnected = connected;
if (connected) {
m_connectButton->setText("断开");
m_connectButton->setObjectName("disconnectButton");
m_connectButton->style()->unpolish(m_connectButton);
m_connectButton->style()->polish(m_connectButton);
// 禁用配置控件
m_portComboBox->setEnabled(false);
m_baudRateComboBox->setEnabled(false);
m_dataBitsComboBox->setEnabled(false);
m_parityComboBox->setEnabled(false);
m_stopBitsComboBox->setEnabled(false);
m_refreshButton->setEnabled(false);
// 启用发送控件
m_sendButton->setEnabled(true);
m_sendLineEdit->setEnabled(true);
m_autoSendButton->setEnabled(true);
} else {
m_connectButton->setText("连接");
m_connectButton->setObjectName("connectButton");
m_connectButton->style()->unpolish(m_connectButton);
m_connectButton->style()->polish(m_connectButton);
// 启用配置控件
m_portComboBox->setEnabled(true);
m_baudRateComboBox->setEnabled(true);
m_dataBitsComboBox->setEnabled(true);
m_parityComboBox->setEnabled(true);
m_stopBitsComboBox->setEnabled(true);
m_refreshButton->setEnabled(true);
// 禁用发送控件
m_sendButton->setEnabled(false);
m_sendLineEdit->setEnabled(false);
m_autoSendButton->setEnabled(false);
}
}
/*
* ============================================================================
* 数据发送函数
*
* 学习要点:
* 1. 函数开始前进行必要的状态检查
* 2. 根据发送模式选择不同的数据处理方式
* 3. 错误处理和用户提示
* 4. 数据统计和界面更新
* 5. 发送模式与显示模式的区别
* ============================================================================
*/
void Widget::sendData()
{
/*
* 第一步:检查串口连接状态
*
* 学习要点:
* - 在执行操作前先检查前置条件
* - 使用QMessageBox提供用户友好的错误提示
* - return语句提前退出函数
*/
if (!m_serial->isOpen()) {
QMessageBox::warning(this, "错误", "串口未连接!");
return; // 串口未连接,直接返回
}
/*
* 第二步:获取用户输入的数据
*
* 学习要点:
* - 从界面控件获取用户输入
* - 检查输入是否为空
* - QString::isEmpty()判断字符串是否为空
*/
QString text = m_sendLineEdit->text(); // 获取输入框中的文本
if (text.isEmpty()) {
return; // 输入为空,直接返回
}
/*
* 第三步:根据发送模式处理数据
*
* 学习要点:
* - 发送模式决定如何解析用户输入
* - 二进制模式:解析十六进制字符串为字节数组
* - ASCII模式:将字符串转换为UTF-8字节数组
* - 错误处理:无效的HEX格式需要提示用户
*/
QByteArray data; // 存储要发送的字节数据
if (m_binarySendMode) {
/*
* 二进制(HEX)模式发送
*
* 学习要点:
* - parseHexString()函数将"01 02 03"格式的字符串转换为字节数组
* - 需要验证HEX格式的有效性
* - 提供具体的错误提示和示例
*/
data = parseHexString(text);
if (data.isEmpty() && !text.isEmpty()) {
// HEX格式错误,提示用户正确的格式
QMessageBox::warning(this, "错误",
"HEX格式错误!\n请输入有效的十六进制数据,如: 01 02 03 或 010203");
return;
}
} else {
/*
* ASCII模式发送
*
* 学习要点:
* - toUtf8()将QString转换为UTF-8编码的QByteArray
* - UTF-8是Unicode的一种编码方式,兼容ASCII
* - 支持中文等多字节字符
*/
data = text.toUtf8();
}
/*
* 第四步:发送数据到串口
*
* 学习要点:
* - QSerialPort::write()返回实际写入的字节数
* - 返回值可能小于请求写入的字节数
* - 需要检查返回值判断是否发送成功
*/
qint64 bytesWritten = m_serial->write(data);
if (bytesWritten > 0) {
/*
* 发送成功的处理
*
* 学习要点:
* - 更新发送字节数统计
* - 在界面上显示发送的数据
* - 根据显示模式格式化数据显示
*/
// 更新发送字节数统计
m_txBytes += bytesWritten;
updateStatistics(); // 更新统计信息显示
/*
* 格式化并显示发送的数据
*
* 学习要点:
* - 发送模式决定如何解析数据
* - 显示模式决定如何在界面上显示数据
* - 这两个概念是独立的
* - formatData()函数根据显示模式格式化数据
* - appendToDisplay()函数将文本添加到显示区域
*/
QString displayText = formatData(data, m_txHexCheckBox->isChecked());
appendToDisplay(QString("[发送] %1").arg(displayText), "#e67e22");
/*
* 可选:清空输入框
*
* 学习要点:
* - 有些用户希望发送后清空输入框
* - 有些用户希望保留输入内容以便重复发送
* - 这里注释掉,保留用户输入
*/
// m_sendLineEdit->clear();
} else {
/*
* 发送失败的处理
*
* 学习要点:
* - 网络或硬件问题可能导致发送失败
* - 需要提示用户发送失败
* - 可以考虑重试机制(这里简化处理)
*/
QMessageBox::warning(this, "发送失败", "数据发送失败!");
}
}
void Widget::readSerialData()
{
QByteArray data = m_serial->readAll();
if (data.isEmpty()) {
return;
}
m_rxBytes += data.size();
updateStatistics();
QString displayText = formatData(data, m_rxHexCheckBox->isChecked());
appendToDisplay(QString("[接收] %1").arg(displayText), "#3498db");
}
void Widget::appendToDisplay(const QString &text, const QString &color)
{
QString timestamp = m_timestampCheckBox->isChecked() ?
QString("[%1] ").arg(QDateTime::currentDateTime().toString("hh:mm:ss.zzz")) : "";
QString formattedText = QString("<span style='color: %1;'>%2%3</span>")
.arg(color)
.arg(timestamp)
.arg(text);
m_displayArea->append(formattedText);
// 自动滚动到底部
QScrollBar *scrollBar = m_displayArea->verticalScrollBar();
scrollBar->setValue(scrollBar->maximum());
}
QString Widget::formatData(const QByteArray &data, bool isHex)
{
if (isHex) {
return data.toHex(' ').toUpper();
} else {
// 处理不可打印字符
QString result;
for (char c : data) {
if (c >= 32 && c <= 126) {
result += c;
} else {
result += QString("[%1]").arg(static_cast<unsigned char>(c), 2, 16, QChar('0')).toUpper();
}
}
return result;
}
}
QByteArray Widget::parseHexString(const QString &hexStr)
{
QString cleanHex = hexStr;
cleanHex.remove(' ').remove('\t').remove('\n').remove('\r');
if (cleanHex.length() % 2 != 0) {
return QByteArray(); // 无效的HEX字符串
}
QByteArray result;
bool ok;
for (int i = 0; i < cleanHex.length(); i += 2) {
QString byteStr = cleanHex.mid(i, 2);
quint8 byte = byteStr.toUInt(&ok, 16);
if (!ok) {
return QByteArray(); // 无效的HEX字符
}
result.append(byte);
}
return result;
}
void Widget::updateStatistics()
{
m_rxBytesLabel->setText(QString("接收: %1 字节").arg(m_rxBytes));
m_txBytesLabel->setText(QString("发送: %1 字节").arg(m_txBytes));
}
void Widget::clearReceiveArea()
{
// 这里需要实现清除接收数据的逻辑
// 由于我们现在使用统一的显示区域,可以考虑标记或过滤
QMessageBox::information(this, "提示", "接收数据已清除");
}
void Widget::clearSendArea()
{
// 这里需要实现清除发送数据的逻辑
QMessageBox::information(this, "提示", "发送数据已清除");
}