使用 wxWidgets 中的资源、定时器和 COM 端口






4.79/5 (9投票s)
wxWidgets 资源示例和 COM 端口通信。
引言
在本文中,我将解释如何在一个窗口中放置一些基本的资源来与用户交互,例如按钮、文本字段和组合框。
作为奖励,我将添加一个简单的 COM 端口库,并以一个非常基础的单行终端应用程序为例。
我将解释如何使用 wxWidget 事件驱动的定时器来自动化某些操作,例如读取串行输入缓冲区并将信息显示在文本字段中。
其思路是逐步解释每个对象的代码,从窗口的布局开始,然后逐步添加不同的函数和必要的代码。
背景
本文的示例代码是用 Code::Blocks 和 wxWIdgets 3.0.1 制作的。您可以在本文中看到如何下载和编译它
使用 wxSmith 进行 wxWidgets GUI 编程入门
串行通信是通过一个简单的库实现的,您可以在本文中找到该库,其中简要介绍了如何使用它。我在这篇文章中也解释了如何使用它
但与那里使用的列出端口的库不同,我将在这里使用另一种枚举端口的方法。
别担心,我会详细解释每个方面。
代码
开始的最佳方式是从头开始,所以打开 Code::Blocks IDE 并创建一个新的 wxWidgets 项目。在“背景”部分引用的第一篇文章中,您可以找到有关在 C::B 中创建 wxProject 的指南。按照这些步骤创建新项目。
创建新的 wxWidgets 项目时看到的第一个屏幕如下
这是资源面板。在这里,我们可以拖放控件和资源来构建屏幕。
布局
首先,我们将创建布局。所以我们会向屏幕添加一些对象。但首先,我们必须做一些事情来分隔和组织布局。首先添加一个 wxBoxSizer。Sizers 为我们完成了组织屏幕上元素的工作。然后将一个 wxPanel 添加到 BoxSizer 中。现在将一个 wxGridSizer 添加到面板中。有关如何执行此操作的更多参考,请参阅前面引用的文章。
wxGridSizer 允许我们将元素添加到矩阵中,而不是像 BoxSizer 那样添加到一行中。如果您从左侧的资源树中选择 GridSizer,您将在树下方的属性菜单中看到其属性。在 GridSizer 属性中,您可以设置行数和列数。
示例包括一个可用 COM 端口列表,您可以在其中选择要连接的端口,一个“连接”按钮,一个用于输入要发送的消息的文本字段,以及一个用于显示接收消息的文本字段。为简单起见,我将接收字段设置为单行,但通过在文本字段属性中设置正确的属性,可以轻松将其设为多行。
一旦我们在屏幕上设置了这些 Sizer,我们将继续构建屏幕。目标屏幕如下
正如您在 GridSizer 的属性中看到的,我指定了 3 行 2 列。
现在我们必须添加对象。这可以通过单击对象栏中的对象,然后单击图表来完成。首先单击“标准”栏中的 wxComboBox 图标,然后单击我们屏幕上的小正方形。现在我们有了未来的 COM 端口列表。单击它,然后在左侧的属性列表中,我们可以看到一些可以更改的属性。要更改的主要属性是对象名称,以便我们可以引用它并使代码更易于维护。
正如您在屏幕截图中看到的,我将 **变量名** 更改为 comSel,将 **标识符** 更改为 ID_COMSEL。
然后将按钮添加到 comboBox 的右侧,将其 **变量名** 更改为 conBut,将其 **标签** 更改为 Connect
现在添加一个 wxTextCtrl。我将其 **变量名** 更改为 mesBox,将其 **标签** 更改为 Message,将其 **标识符** 更改为 ID_MESBOX。
添加另一个 wxButton。**标签:** Send,**变量名:** sendBut,**标识符:** ID_SENDBUT。
现在添加一个 wxStaticText,**标签:** Received,**变量名:** recText,**标识符:** ID_RECTEXT
最后添加一个 wxTextCtrl。**标签:** 空,因为在这里我们将显示传入的消息,**变量名:** recBox,**标识符:** ID_RECBOX。我们将通过在属性编辑器中的样式选项中设置此属性来将其定义为只读。
请记住,稍后当我们定义与屏幕上的元素交互的函数时,这些函数将引用对象的变量名。
现在您可以构建项目以查看它是否正常工作。如果一切正常,我们应该会看到类似这样的结果
但当然,这没有任何功能。所以让我们添加一些代码来使其工作。
串口枚举器
可用的串行端口枚举是通过 `Find_Comm` 函数进行的,该函数分别在 `portEnum.h` 和 `portsEnum.cpp` 中声明和实现。请看代码
#include <string>
#include "portsEnum.h"
#include "windows.h"
using namespace std;
int Find_Comm(string* ports)
{
//string ports[5];
HANDLE DriverHandle;
DWORD LastError[4];
char Com_Name[10];
int portsCant = 0;
for(int x = 1 ; x < 10 ; x++)
{
switch(x)
{
case 1:
strcpy(Com_Name,"COM1");
break;
case 2:
strcpy(Com_Name,"COM2");
break;
case 3:
strcpy(Com_Name,"COM3");
break;
case 4:
strcpy(Com_Name,"COM4");
break;
case 5:
strcpy(Com_Name,"COM5");
break;
case 6:
strcpy(Com_Name,"COM6");
break;
case 7:
strcpy(Com_Name,"COM7");
break;
case 8:
strcpy(Com_Name,"COM8");
break;
case 9:
strcpy(Com_Name,"COM9");
break;
}
DriverHandle = CreateFile (Com_Name, 0, 0, NULL, OPEN_EXISTING, 0, NULL);
LastError[x-1] = GetLastError();
if(LastError[x-1] == 0)
{
ports[portsCant]=Com_Name;
portsCant++;
}
CloseHandle(DriverHandle);
}
return portsCant;
}
我标记了两个重要的变量。`ports`,包含可用端口名称的 estring 向量,以及 `portscant`,可用端口的数量。
为了在我们的代码中使用它,我首先在 `wxlConsoleMain.h` 文件中的 `wxlConsoleFrame` 类声明中声明了两个变量,`comports` 和 `port_nr`。我将变量声明在那里,以便在帧对象的函数中可用。
class wxlConsoleFrame: public wxFrame
{
public:
string comports[10];
int port_nr;
非常重要:切勿修改 `//(*` 和 `//*)` 之间的代码。此代码由 wxWidgets 和 IDE 自动生成和修改。
然后,在 `wxlConsoleMain.cpp` 的 `wxlConsoleFrame` 构造函数 `wxlConsoleFrame::wxlConsoleFrame(wxWindow* parent,wxWindowID id)` 中,我们必须调用端口枚举器函数,这样当帧创建时,我们已经列出了端口
port_nr = 0; // Port number initialize
int portnum; // Number of available ports
portnum = Find_Comm(comports); // Call to the ports finder function
for(int i=0; i<portnum; i++)
{
comSel->Append(comports[i]); // Showing the available ports in the comSel combo box
}
`Find_comm` 函数要求一个字符串指针作为参数,并以整数形式返回端口数量。
这里最重要的是在 for 循环中与 GUI 对象进行首次交互。在那里,我们使用 wxComboBox 成员函数 `Append(string)` 用可用端口填充组合框 `comSel`。每次调用 `Append` 都会为组合框列表添加一个新的字符串选项。
我们可以构建项目并验证它是否正常工作。我们有一个可用端口列表,现在我们必须选择一个并连接到它。
选择和打开串口
我们希望用户可以从列表中选择一个端口,并在按下“连接”按钮时连接到它。当用户选择组合框中的一个选项时,会触发一个事件,但我们在此情况下不使用它,因为连接将在按下连接按钮时进行。所以我们必须定义当按下连接按钮时会发生什么。
在资源视图(`wxlConsoleFrame.wxs`)中双击连接按钮,我们直接访问 `wxlConsoleMain.cpp` 中的 `wxlConsoleFrame::OnconButClick` 事件函数(您可能需要向下滚动才能找到它)。在此函数中,我们可以定义按下按钮时发生的动作。这些动作将从组合框中获取选定的字符串(选定的端口)并打开端口。请注意,当您双击按钮以生成事件时,它会自动在 `wxlConsoleMain.h` 文件中的 `wxlConsoleFrame` 类中生成函数声明。
void wxlConsoleFrame::OnconButClick(wxCommandEvent& event)
{
wxString comSelected = comSel->GetStringSelection(); // Get the selected string form the combo box
int port_aux = 0;
if(comSelected.size() > 1) // If the selection is not void
{
port_aux = (int) comSelected[3] - 48; // The string is in the form 'COMn'
if(port_nr == 0)
{
if(!RS232_OpenComport((port_aux-1), baudrate)) // If the port can be open
{
port_nr = port_aux; // Store the port number
mesBox->SetValue((char)(port_aux+48)); // Show the port value in the receive box
}
}
}
}
首先,使用组合框成员函数 `GetStringSelected()` 获取选定的字符串。它返回一个 wxString,其中包含组合框中选定的文本。组合框还有另一个返回选定项顺序号的成员函数。
然后检查选择是否不为空并获取端口号。组合框中的字符串格式为“COMn”,其中“n”是端口号,因此我们取字符串的第四个字符,将其转换为 int 并减去 48(ASCII 中的“0”).
条件 `post_nr==0` 用于检查之前是否没有打开任何端口。我在代码的其他部分使用它来实现相同的目的。
RS232_OpenComport((port_aux-1), baudrate))
如果端口成功打开,则返回 0。`baudrate` 被声明为 `wxlConsoleFrame` 类在 `wxlConsoleMain.h` 文件中的一个成员,并在构造函数中初始化为 9600。
如果端口可以打开,则存储端口号并将其显示在接收消息框中,以通知它已打开,方法是使用 TexCtrl 成员函数 `setValue(string)`。您可以使用 `AppendText(string)` 追加一条消息,例如“port n succefully opened” 而不是。
mesBox->SetValue("Port ");
mesBox->AppendText((char)(port_aux+48));
mesBox->AppendText(" succefuly.");
或使用格式化打印
wxString auxSt;
auxSt.Printf("Port %d opened", port_nr);
recBox->SetValue(auxSt);
将消息发送到选定端口
端口打开后,我们希望与其通信。当按下发送按钮时,我们需要获取写入发送消息文本字段的文本并将其写入端口。操作是在按下按钮时执行的,所以我们必须在发送按钮事件函数中编写代码。
通过在 GUI 视图(`wxlConsoleFrame.wxs`)中双击发送按钮,我们会进入 `wxlConsoleMain.cpp` 中的 `wxlConsoleFrame::OnsendButClick`(同样,您可能需要向下滚动才能找到它)。在这里,我们将添加代码以获取文本字段中的文本并将其写入端口。
void wxlConsoleFrame::OnsendButClick(wxCommandEvent& event)
{
wxString sendMesg = mesBox->GetValue(); // Get the text from the text field in the window
string sended;
sended = sendMesg.ToStdString(); // Convert to standard C++ string
if(port_nr > 0)
RS232_SendBuf(port_nr-1,(char*)sended.c_str(),sended.size());//Write the text in the port
}
首先,我们使用成员函数 `GetValue()` 获取用户在 `mesBox` TextCtrl 中写入的文本,并将其赋给一个辅助的 wxString 对象。
因为 RS232 库是用 C 编写的,所以我们必须给它一个经典的 C 风格字符串(char*)作为参数,所以我们必须首先将 wxString 转换为标准的 C++ 字符串,然后将 `RS232_SendBuf` 的 char* casted `c_str()` 值传递给它。
请注意,在发送消息之前,程序会检查端口是否已打开。
接收和显示传入消息
在通信的另一端,我们希望显示接收到的消息。我们可以通过一个按钮来实现,或者在发送按钮按下时执行相同的事件。但也许我们连接到 PC 的设备会自动发出消息,并且我们希望在消息到达时自动显示它们。由于 RS232 库不是事件驱动的,因此我们必须自动化消息轮询。为了做到这一点,我们将使用定时器,因此作为本教程的奖励,我们将了解如何实现和使用 wxWidgets 事件驱动的定时器。
创建定时器
我们将使用 `wxTimer` 类。它为我们提供了可以设置定时器在发送事件之间间隔的定时器。我们将使用该事件来执行操作。但我们一步一步来。
首先,我们必须创建定时器对象。所以向我们的 Frame 类添加一个定时器对象。在 `wlConsoleMain.h` 中,添加到 `wxlConsoleFrame` 类声明中。
wxTimer* recTimer; // declaration of Timer object
void OnRecTimer(wxTimerEvent& event); // declaration of Timer event declaring
我们还必须声明事件函数作为 Frame 类的成员。
现在我们必须通过定义其所有者和事件函数来定义定时器对象。我们将在 `wlConsoleMain.cpp` 的 Frame 类的构造函数中执行此操作。
recTimer = new wxTimer(this, Rec_Timer);
recTimer->Start(interval);
当然,启动定时器。`interval` 在此文件中被定义为一个全局变量,以毫秒为单位。`wxTimer` 构造函数的参数是定时器的所有者对象(在本例中为 `wxlConsoleFrame`)和事件函数。
我们使用定时器的最后一件事是将函数与事件关联。我们在同一个文件 `wlConsoleMain.cpp` 的 `EVENT_TABLE` 中执行此操作。在那里,我们必须添加事件并将其与函数和一个 ID 相关联。
enum // Timer events IDs
{
Rec_Timer = wxID_HIGHEST,
};
BEGIN_EVENT_TABLE(wxlConsoleFrame,wxFrame)
//(*EventTable(wxlConsoleFrame)
//*)
EVT_TIMER(Rec_Timer, wxlConsoleFrame::OnRecTimer) //Receive Timer event declaration
END_EVENT_TABLE()
首先,我们定义事件的 ID(示例中只有一个),然后定义事件表中的事件。我们将 `EVT_TIMER` 事件与 `Rec_Timer` ID 和函数 `wxlConsoleFrame::OnRecTimer` 相关联。
现在,定时器每 200 毫秒(或您初始化定时器时的值)发送一个事件。只需利用此滴答声做些事情。
使用定时器事件显示传入消息
要在定时器事件中执行操作,我们只需在定时器事件函数中编写一些代码。我们想轮询 COM 端口并显示消息(如果有)。
因此,在 `wlConsoleMain.cpp` 中,我们添加了功能。
void wxlConsoleFrame::OnRecTimer(wxTimerEvent& event)
{
//wxString recMesg;
unsigned char recBuff[512];
int readed = 0;
recBuff[0] = 0;
readed = RS232_PollComport
(port_nr-1, recBuff, 512); // Poll the serie's buffer
if(readed > 0)
{
recBuff[readed] = 0; // Finalize the char buffer
recBox->SetValue(recBuff); // Show the message
//recBox->AppendText(recBuff); // Show the message
//recBox->AppendText(_("\n")); // If multiline text insert a carriage return
}
}
在这里,我们首先创建一个 char* 缓冲区来存储消息。如前所述,RS232 库使用 char* C 风格字符串。使用 `RS232_PollComport`,我们检查串行缓冲区中是否有任何消息,如果有,则将其读入缓冲区。此函数返回读取的字节数。
然后,将读取的消息显示在 `recBox` 文本控件中。`Setvalue` 函数将窗口中的当前文本更改为参数传递的文本。如果您想保留文本框中的过去文本,可以使用 `AppendText`,如果您将文本框定义为多行,则可以附加一个回车符,以便每条新消息显示在新行中。
现在我们已经实现了所有功能。只剩下清理工作了。
退出应用程序时进行清理
离开程序后,我们必须停止定时器并关闭端口。所以我们在 `OnQuit` 事件中添加一些代码。
void wxlConsoleFrame::OnQuit(wxCommandEvent& event)
{
recTimer->Stop();
// Stop the timer
if(port_nr > 0)
RS232_CloseComport(port_nr-1); // Closing the port before quit the application
Close();
}
当窗口关闭但应用程序关闭后,会触发此事件。
这是应用程序正在运行的屏幕截图,连接到一个充当回声器的 Arduino 板。
关注点
在本文中,我试图展示如何使用一些 wxWidgets 资源(基本资源),并通过使用 RS232 库和 wxTimer 添加一些有用的额外内容。
另一个非常重要的方面是用于获取和设置 GUI 资源值的函数:**GetValue** 和 **SetValue**。这些函数使我们能够轻松地与我们在屏幕上放置的资源进行交互。