65.9K
CodeProject 正在变化。 阅读更多。
Home

IntelliPort

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (57投票s)

2014年7月21日

GPL3

8分钟阅读

viewsIcon

199266

downloadIcon

1721

您可以使用 IntelliPort 通过串行端口将大文件从一台计算机传输到便携式计算机,而无需设置便携式计算机的网络连接。

HyperTerminal

引言

正如您所知,从 Windows 7 开始,Microsoft 就已停止开发 HyperTerminal。IntelliPort 是一款程序,您可以使用它通过直通电缆或以太网连接到其他计算机。IntelliPort 会记录在连接的另一端计算机之间传递的消息。因此,在设置和使用调制解调器时,它可以作为宝贵的故障排除工具。为了确保您的调制解调器连接正确或查看调制解调器的设置,您可以通过 IntelliPort 发送命令并检查结果。IntelliPort 具有滚动功能,可让您查看已滚出屏幕的已接收文本。您可以使用 IntelliPort 通过串行端口将大文件从一台计算机传输到便携式计算机,而无需设置便携式计算机的网络连接。IntelliPort 被设计为易于使用的工具,并不旨在取代市场上其他功能齐全的工具。您可以使用 IntelliPort 执行上述特定任务,但不要尝试使用 IntelliPort 来满足更复杂的通信需求。

背景

此 MFC 应用程序的主要目的是通过串行端口或 UDP 套接字从嵌入式系统(如 EFTPOS 设备)获取日志,并且它基于 PJ Naughter 的 CSerialPortCWSocket 类。对于不知道的人来说,EFTPOS 是无需携带现金即可支付商品或服务费用的方法。在进行购买时,EFTPOS 客户将 EFTPOS 卡交给收银员,收银员将其插入现场的 EFTPOS 机。当 EFTPOS 客户通过签名或安全 PIN 码确认购买时,EFTPOS 设备会通过电子方式联系商店的银行进行交易。还会向客户的银行发送一条消息。除非 EFTPOS 交易有理由不完成,否则资金将在两个账户之间转移。EFTPOS 交易仅需几秒钟。在 EFTPOS 客户将商品装袋之前,EFTPOS 交易就会完成。EFTPOS 交易的确认会发送到商店,并以打印的 EFTPOS 交易记录的形式传递给客户。

如何开始?

首先,配置 MFC 应用程序以从串行端口或 TCP/UDP 套接字连接获取日志。请检查 CConfigureDlg 类以获取实现细节。

Configure serial port or TCP/UDP socket

接下来,您现在可以连接/断开与数据源的连接。请检查 OnOpenSerialPortOnCloseSerialPort 函数以获取实现细节。

Reading/Writing data to serial port or TCP/UDP socket

void CMainFrame::OnOpenSerialPort()
{
    try
    {
        CString strFormat, strMessage;
        switch (theApp.m_nConnection)
        {
            case 0:
            {
                CString strFullPortName;
                strFullPortName.Format(_T("\\\\.\\%s"), static_cast<LPCWSTR>(theApp.m_strSerialName));
                m_pSerialPort.Open(
                    strFullPortName,
                    theApp.m_nBaudRate,
                    (CSerialPort::Parity) theApp.m_nParity,
                    (BYTE)theApp.m_nDataBits,
                    (CSerialPort::StopBits) theApp.m_nStopBits,
                    (CSerialPort::FlowControl) theApp.m_nFlowControl,
                    FALSE);

                if (m_pSerialPort.IsOpen())
                {
                    m_nThreadRunning = true;
                    m_hSerialPortThread = CreateThread(nullptr, 0, SerialPortThreadFunc, this, 0, &m_nSerialPortThreadID);
                    strFormat.LoadString(IDS_SERIAL_PORT_OPENED);
                    strMessage.Format(strFormat, static_cast<LPCWSTR>(theApp.m_strSerialName));
                    SetCaptionBarText(strMessage);
                }
                break;
            }
            case 1:
            case 2:
            {
                CString strServerIP = theApp.m_strServerIP;
                UINT nServerPort = theApp.m_nServerPort;
                CString strClientIP = theApp.m_strClientIP;
                UINT nClientPort = theApp.m_nClientPort;

                if (theApp.m_nConnection == 1) // TCP Socket
                {
                    if (theApp.m_nSocketType == 1) // Client
                    {
                        m_pSocket.CreateAndConnect(strServerIP, nServerPort);
                    }
                    else // TCP Server
                    {
                        m_pSocket.SetBindAddress(strClientIP);
                        m_pSocket.CreateAndBind(nClientPort, SOCK_STREAM, AF_INET);

                        m_dlgIncoming.ShowWindow(SW_SHOW);
                        m_dlgIncoming.CenterWindow(this);
                        m_dlgIncoming.Invalidate();
                        m_dlgIncoming.UpdateWindow();
                        m_pSocket.Listen();
                        m_pSocket.Accept(m_pIncomming);
                        m_dlgIncoming.ShowWindow(SW_HIDE);
                    }
                }
                else // UDP Socket
                {
                    m_pSocket.SetBindAddress(strClientIP);
                    m_pSocket.CreateAndBind(nClientPort, SOCK_DGRAM, AF_INET);

                    strServerIP = strClientIP;
                    nServerPort = nClientPort;
                }

                if (m_pSocket.IsCreated())
                {
                    m_nThreadRunning = true;
                    m_hSocketThread = CreateThread(nullptr, 0, SocketThreadFunc, this, 0, &m_nSocketTreadID);
                    strFormat.LoadString(IDS_SOCKET_CREATED);
                    strMessage.Format(strFormat, ((theApp.m_nConnection == 1) ? _T("TCP") : _T("UDP")), static_cast<LPCWSTR>(strServerIP), nServerPort);
                    SetCaptionBarText(strMessage);
                }
                break;
            }
        }
    }
    catch (CSerialException& pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException.GetErrorMessage2(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        // pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
    catch (CWSocketException* pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException->GetErrorMessage(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
}

void CMainFrame::OnCloseSerialPort()
{
    if (m_nThreadRunning)
    {
        m_nThreadRunning = false;
        DWORD nThreadCount = 0;
        HANDLE hThreadArray[2] = { 0, 0 };
        if (m_hSerialPortThread != nullptr)
        {
            hThreadArray[nThreadCount++] = m_hSerialPortThread;
        }
        if (m_hSocketThread != nullptr)
        {
            hThreadArray[nThreadCount++] = m_hSocketThread;
        }
        if (nThreadCount > 0)
        {
            WaitForMultipleObjects(nThreadCount, hThreadArray, TRUE, INFINITE);
        }
    }

    try
    {
        CString strFormat, strMessage;
        switch (theApp.m_nConnection)
        {
            case 0:
            {
                if (!m_pSerialPort.IsOpen())
                {
                    strFormat.LoadString(IDS_SERIAL_PORT_CLOSED);
                    strMessage.Format(strFormat, static_cast<LPCWSTR>(theApp.m_strSerialName));
                    SetCaptionBarText(strMessage);
                }
                break;
            }
            case 1:
            case 2:
            {
                CString strServerIP = theApp.m_strServerIP;
                UINT nServerPort = theApp.m_nServerPort;
                CString strClientIP = theApp.m_strClientIP;
                UINT nClientPort = theApp.m_nClientPort;

                if (!m_pSocket.IsCreated())
                {
                    if (theApp.m_nConnection == 1) // TCP Socket
                    {
                        if (theApp.m_nSocketType == 1) // Client
                        {
                        }
                        else // TCP Server
                        {
                        }
                    }
                    else // UDP Socket
                    {
                        strServerIP = strClientIP;
                        nServerPort = nClientPort;
                    }

                    strFormat.LoadString(IDS_SOCKET_CLOSED);
                    strMessage.Format(strFormat, ((theApp.m_nConnection == 1) ? _T("TCP") : _T("UDP")), static_cast<LPCWSTR>(strServerIP), nServerPort);
                    SetCaptionBarText(strMessage);
                }
                break;
            }
        }
    }
    catch (CSerialException& pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException.GetErrorMessage2(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        // pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
    catch (CWSocketException* pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException->GetErrorMessage(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
}

void CMainFrame::OnSendReceive()
{
    try
    {
        CInputDlg dlgInput(this);
        if (dlgInput.DoModal() == IDOK)
        {
            CStringA pBuffer(dlgInput.m_strSendData);
            const int nLength = pBuffer.GetLength();

            switch (theApp.m_nConnection)
            {
                case 0:
                {
                    m_pMutualAccess.lock();
                    m_pSerialPort.Write(pBuffer.GetBufferSetLength(nLength), nLength);
                    m_pMutualAccess.unlock();
                    pBuffer.ReleaseBuffer();
                    break;
                }
                case 1:
                case 2:
                {
                    CString strServerIP = theApp.m_strServerIP;
                    const UINT nServerPort = theApp.m_nServerPort;

                    if (theApp.m_nConnection == 1) // TCP Socket
                    {
                        if (theApp.m_nSocketType == 1) // Client
                        {
                            if (m_pSocket.IsWritable(1000))
                            {
                                m_pMutualAccess.lock();
                                m_pSocket.Send(pBuffer.GetBufferSetLength(nLength), nLength, 0);
                                pBuffer.ReleaseBuffer();
                                m_pMutualAccess.unlock();
                            }
                        }
                        else
                        {
                            if (m_pIncomming.IsWritable(1000))
                            {
                                m_pMutualAccess.lock();
                                m_pIncomming.Send(pBuffer.GetBufferSetLength(nLength), nLength, 0);
                                pBuffer.ReleaseBuffer();
                                m_pMutualAccess.unlock();
                            }
                        }
                    }
                    else
                    {
                        if (m_pSocket.IsWritable(1000))
                        {
                            m_pMutualAccess.lock();
                            m_pSocket.SendTo(pBuffer.GetBufferSetLength(nLength), nLength, nServerPort, strServerIP, 0);
                            pBuffer.ReleaseBuffer();
                            m_pMutualAccess.unlock();
                        }
                    }
                    break;
                }
            }
        }
    }
    catch (CSerialException& pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException.GetErrorMessage2(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        // pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
    catch (CWSocketException* pException)
    {
        const int nErrorLength = 0x100;
        TCHAR lpszErrorMessage[nErrorLength] = { 0, };
        pException->GetErrorMessage(lpszErrorMessage, nErrorLength);
        TRACE(_T("%s\n"), lpszErrorMessage);
        pException->Delete();
        SetCaptionBarText(lpszErrorMessage);
        m_nThreadRunning = false;
    }
}

关注点

为了保持 GUI 响应,MFC 应用程序在单独的工作线程中进行读取

DWORD WINAPI SerialPortThreadFunc(LPVOID pParam)
{
    COMSTAT status = { 0, };
    char pBuffer[0x10000] = { 0, };
    CMainFrame* pMainFrame = (CMainFrame*) pParam;
    CRingBuffer& pRingBuffer = pMainFrame->m_pRingBuffer;
    CSerialPort& pSerialPort = pMainFrame->m_pSerialPort;
    std::mutex& pMutualAccess = pMainFrame->m_pMutualAccess;

    while (pMainFrame->m_nThreadRunning)
    {
        try
        {
            memset(&status, 0, sizeof(status));
            pSerialPort.GetStatus(status);
            if (status.cbInQue > 0)
            {
                memset(pBuffer, 0, sizeof(pBuffer));
                const int nLength = pSerialPort.Read(pBuffer, sizeof(pBuffer));
                pMutualAccess.lock();
                pRingBuffer.WriteBinary(pBuffer, nLength);
                pMutualAccess.unlock();
            }
            else
            {
                ::Sleep(10);
            }
        }
        catch (CSerialException& pException)
        {
            const int nErrorLength = 0x100;
            TCHAR lpszErrorMessage[nErrorLength] = { 0, };
            pException.GetErrorMessage2(lpszErrorMessage, nErrorLength);
            TRACE(_T("%s\n"), lpszErrorMessage);
            // pException->Delete();
            pMainFrame->SetCaptionBarText(lpszErrorMessage);
            pMainFrame->m_nThreadRunning = false;
            pSerialPort.Close();
        }
    }
    pSerialPort.Close();
    return 0;
}

DWORD WINAPI SocketThreadFunc(LPVOID pParam)
{
    char pBuffer[0x10000] = { 0, };
    CMainFrame* pMainFrame = (CMainFrame*) pParam;
    CRingBuffer& pRingBuffer = pMainFrame->m_pRingBuffer;
    CWSocket& pSocket = pMainFrame->m_pSocket;
    CWSocket& pIncomming = pMainFrame->m_pIncomming;
    std::mutex& pMutualAccess = pMainFrame->m_pMutualAccess;
    bool bIsTCP = (theApp.m_nConnection == 1);
    bool bIsClient = (theApp.m_nSocketType == 1);

    CString strServerIP = theApp.m_strServerIP;
    UINT nServerPort = theApp.m_nServerPort;

    while (pMainFrame->m_nThreadRunning)
    {
        try
        {
            if (bIsTCP)
            {
                if (bIsClient)
                {
                    if (pSocket.IsReadible(1000))
                    {
                        memset(pBuffer, 0, sizeof(pBuffer));
                        const int nLength = pSocket.Receive(pBuffer, sizeof(pBuffer), 0);
                        pMutualAccess.lock();
                        pRingBuffer.WriteBinary(pBuffer, nLength);
                        pMutualAccess.unlock();
                    }
                    else
                    {
                        ::Sleep(10);
                    }
                }
                else
                {
                    if (pIncomming.IsReadible(1000))
                    {
                        memset(pBuffer, 0, sizeof(pBuffer));
                        const int nLength = pIncomming.Receive(pBuffer, sizeof(pBuffer), 0);
                        pMutualAccess.lock();
                        pRingBuffer.WriteBinary(pBuffer, nLength);
                        pMutualAccess.unlock();
                    }
                    else
                    {
                        ::Sleep(10);
                    }
                }
            }
            else
            {
                if (pSocket.IsReadible(1000))
                {
                    memset(pBuffer, 0, sizeof(pBuffer));
                    const int nLength = pSocket.ReceiveFrom(pBuffer, sizeof(pBuffer), strServerIP, nServerPort, 0);
                    pMutualAccess.lock();
                    pRingBuffer.WriteBinary(pBuffer, nLength);
                    pMutualAccess.unlock();
                }
                else
                {
                    ::Sleep(10);
                }
            }
        }
        catch (CWSocketException* pException)
        {
            const int nErrorLength = 0x100;
            TCHAR lpszErrorMessage[nErrorLength] = { 0, };
            pException->GetErrorMessage(lpszErrorMessage, nErrorLength);
            TRACE(_T("%s\n"), lpszErrorMessage);
            pException->Delete();
            pMainFrame->SetCaptionBarText(lpszErrorMessage);
            pMainFrame->m_nThreadRunning = false;
            pIncomming.Close();
            pSocket.Close();
        }
    }
    pIncomming.Close();
    pSocket.Close();
    return 0;
}

结束语

IntelliPort 应用程序使用了许多在 Code Project 上发布的组件。非常感谢

  • PJ Naughter 的 EnumSerialPorts
  • Larry Antram 的 CRingBuffer
  • PJ Naughter 的 CSerialPort
  • PJ Naughter 的 CWSocket
  • PJ Naughter 提供的CVersionInfo

后续计划:我想尽快添加 TelnetSSH 支持。

USB 端口取代串行端口的 3 个原因

最初的 IBM PC 包含 RS-232 串行端口,帮助推广了一种持续了数十年的数据传输格式。成千上万的工业设备,从流量计到其他类型的实验室仪器,传统上都将 RS-232 作为标准输入/输出端口。2004 年,大多数新 PC 已不再将 RS-232 串行接口作为其标准或基本配置的一部分。常见的 PC 桌面外围设备,如打印机、扫描仪和传真机,目前都采用 USB 端口。

通用串行总线(Universal Serial Bus,简称 USB)是一种外部端口,可在外部设备和计算机之间进行接口。最初的 IBM 个人计算机带有 RS-232 端口,用于连接键盘或鼠标等外部设备。如今,USB 端口正在取代 RS-232 端口。几乎任何设备都可以插入 USB 端口。这包括键盘、相机、鼠标、游戏杆、调制解调器、Zip 驱动器、软盘驱动器、打印机和扫描仪。

USB 为外设制造商提供了三个主要优势

  • 兼容性:在过去几年里,串行端口几乎从 PC 上消失了,USB 端口取而代之。有数千种工业设备仍然使用串行端口,这一变化正导致问题。幸运的是,您可以购买便宜的适配器,允许您将 USB 端口连接到串行设备。这些设备效果很好,但它们只是权宜之计。
  • 速度:USB 的平均数据传输速度是普通并行端口的十倍。它也比串行端口快。平均串行端口传输速率为 150 kbps;USB 端口最高可达 12 Mbps。USB 2 的速度是 USB 1 的四十倍,最高传输速率为 480 Mbps。它向后兼容 USB 1。这意味着,如果新计算机配备 USB 2,旧的 USB 设备仍然可以使用。当然,它们将以 USB 1 的速度运行,但它们仍然可以正常工作。
  • 耐用性:USB 端口比串行端口更坚固。串行端口不坚固,细小的针脚很容易弯曲或折断。另一方面,USB 端口非常坚固。

 


如果您的系统有空闲的 USB 端口,您可以在 USB 和串行信号之间进行转换。USB 转串行适配器是一种小型设备,一端是 USB 连接器,另一端至少有一个(可能多个)串行连接器。

可以使用 USB 进行串行通信吗? 是的,您可以使用 USB-to-serial 适配器进行串行通信。此适配器允许通过串行端口通信的设备连接到计算机的 USB 端口。这是连接旧设备(如调制解调器、工业设备和某些类型的微控制器)到可能缺少原生串行端口的现代计算机的常见解决方案。

历史

  • 版本 1.3 (2014 年 7 月 20 日):初始发布。
  • 版本 1.5 (2014 年 7 月 28 日):修复了串行端口和 UDP 套接字日志功能的多个错误。
  • 版本 1.6 (2015 年 8 月 30 日):修复了 Microsoft Windows 10 64 位版本的问题。
  • 版本 1.7 (2019 年 4 月 3 日):性能和安全修复。
  • 版本 1.8 (2019 年 6 月 15 日)
    • 添加了罗马尼亚语翻译;
    • 添加了 PJ Naughter 的 CInstanceChecker 类。
  • 版本 1.9 (2019 年 7 月 27 日):修复了串行端口名称的错误:请参阅评论中提到的文章。
  • 将源代码从 CodeProject 迁移到 GitLab (2019 年 12 月 7 日)。
  • 版本 1.10 (2020 年 3 月 25 日):更改了主对话框和输入对话框的字体大小。
  • 版本 1.11 (2020 年 5 月 9 日):添加了法语翻译,感谢 Stefan Gaftoniuc。
  • 版本 1.12 (2020 年 6 月 6 日):添加了意大利语翻译,感谢 InterLingua
  • 版本 1.13 (2020 年 6 月 13 日):添加了德语翻译,感谢 InterLingua
  • 版本 1.14 (2020 年 6 月 20 日):添加了西班牙语翻译,感谢 InterLingua
  • 版本 1.15 (2020 年 7 月 19 日):添加了俄语翻译,感谢 InterLingua
  • 版本 1.16 (2020 年 7 月 31 日):添加了希腊语翻译,感谢 InterLingua
  • 版本 1.17 (2020 年 9 月 12 日):进行了改进并修复了错误,使 #IntelliPort 对您来说更好
    • 将 PJ Naughter 的 CSerialPort 库更新到最新版本;
    • 将 PJ Naughter 的 CWSocket 库更新到最新版本。
  • 版本 1.18 (2020 年 9 月 25 日):覆盖了 CEditCtrl 的 64K 限制。
  • 版本 1.19 (2022 年 1 月 7 日):使用新的电子邮件地址更新了“关于”对话框。
  • 版本 1.20 (2022 年 1 月 14 日):将 PJ Naughter 的 CVersionInfo 库更新到最新版本。
  • 版本 1.21 (2022 年 2 月 4 日):更改了外部网站地址。
  • 版本 1.22 (2022 年 2 月 11 日):修复了法语、意大利语、德语、西班牙语、俄语、希腊语翻译中的“文件打开”/“另存为”的关键错误。
  • 版本 1.23 (2022 年 4 月 28 日):将 LICENSE 添加到安装文件夹。
  • 版本 1.24 (2022 年 5 月 12 日):将所有行尾符转换为 Windows 格式 (CR LF)。
  • 版本 1.25 (2022 年 5 月 19 日):将 PJ Naughter 的 CEnumerateSerial 库更新到最新版本。
  • 版本 1.26 (2022 年 5 月 24 日):修复了次要错误。
  • 版本 1.27 (2022 年 9 月 9 日):向 AboutBox 对话框添加了贡献者超链接。
  • 2022 年 12 月 23 日:将源代码从 GitLab 迁移到 GitHub。
  • 版本 1.28 (2023 年 1 月 20 日):删除了 PJ Naughter 的单实例类。
  • 版本 1.29 (2023 年 1 月 23 日):将 PJ Naughter 的 CVersionInfo 库更新到最新版本。
    更新了代码,将 C++ 统一初始化用于所有变量声明。
  • 将代码库中的 NULL 替换为 nullptr
    将代码库中的 BOOL 替换为 bool
    这意味着该应用程序的最低要求现在是Microsoft Visual C++ 2010。
  • 版本 1.30 (2023 年 4 月 2 日):实现了套接字和串行端口连接的错误处理。
  • 版本 1.31 (2023 年 4 月 13 日):重新设计了线程同步并删除了所有 Sleep 调用。
  • 版本 1.32 (2023 年 5 月 27 日):在“关于”对话框中添加了 GPLv3 通知。
  • 版本 1.33 (2023 年 6 月 13 日):使应用程序的设置持久化(由 wvd_vegt 请求)。
  • 版本 1.34 (2023 年 6 月 22 日):将 PJ Naughter 的 CEnumerateSerial 库更新到最新版本。
  • 版本 1.35 (2023 年 7 月 22 日):用 PJ Naughter 的 CHLinkCtrl 库替换了旧的 CHyperlinkStatic 类。
  • 版本 1.36 (2023 年 9 月 29 日)
    • 切换到 Visual Studio Enterprise 2022(源代码做了一些更改);
    • 更改了文章的下载链接。更新了“关于”对话框(电子邮件和网站)。
  • 版本 1.37 (2024 年 1 月 3 日)
    • 添加了社交媒体链接:Twitter、LinkedIn、Facebook 和 Instagram;
    • 添加了 GitHub 仓库的 Issues、Discussions 和 Wiki 的快捷方式。
  • 添加了“USB 端口取代串行端口的 3 个原因”部分。
  • 版本 1.38 (2024 年 1 月 27 日):将 ReleaseNotes.html 和 SoftwareContentRegister.html 添加到 GitHub 仓库。
  • 版本 1.39 (2024 年 2 月 21 日):将 MFC 应用程序的主题切换回原生 Windows。
  • 版本 1.40.1 (2024 年 9 月 26 日)
    • 改进了 UTF8 格式文本的加载/保存/发送/接收。
    • 在“帮助”菜单中实现了 用户手册 选项。
    • 在帮助菜单中添加了检查更新…选项。
© . All rights reserved.