• Qt 编写的程序如何只能运行一个实例


    Qt 编写的程序如何只能运行一个实例

    最近有个小项目,客户要求程序只能运行一个实例。以前没遇到过这种要求,这次特意花了点时间研究了一下。

    大概想了一下,有两种思路。一种是直接去找这个程序已经运行的线索。比如:

    1. 查找系统的进程列表,看看有没有同名的进程已经运行了。有的话说明有其他实例在运行。
    2. 查找有没同名的窗体。有的话说明有其他实例在运行。

    另一种思路是在程序中创造一种条件,这个条件可以被其他的实例感知。比如:

    1. 程序启动时新建一个文件,退出时删除这个文件。启动时如果有同名的文件了就说明有实例已经运行了。
    2. 启动时网络通讯监听某个特定的端口,我们知道一个端口只能被一个程序监听。所以如果监听失败了,就说明有其他实例在运行。
    3. 创建个共享内存对象,如果有同名的共享内存对象存在,就无法创建成功,说明有其他实例在运行。

    查找同名进程

    Qt 没有直接提供读取系统中现有进程的信息的方法。我也没找到有什么第三方库可以跨平台的做这个事情。我现在的办法就是调用 WINDOWS 的 API 去获取这些信息。所以这个代码只针对 WINDOWS 有效,不可移植。

    为了方便,我写了一个辅助类: WinProcessInfo

    这个类的头文件如下:

    #ifndef WINPROCESSINFO_H
    #define WINPROCESSINFO_H
    
    #include 
    #include 
    #include 
    #include 
    
    class WinProcessInfo
    {
    public:
        WinProcessInfo();
        static QString PIDtoName(DWORD pid);
        static QStringList listNames(bool removeUnknown = true);
        static QVector listPID();
        static QVector nameToPID(QString name);
    };
    
    
    #endif // WINPROCESSINFO_H
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    类的实现文件如下:

    #include "WinProcessInfo.h"
    
    WinProcessInfo::WinProcessInfo()
    {
    
    }
    
    QString WinProcessInfo::PIDtoName(DWORD processID)
    {
        TCHAR szProcessName[MAX_PATH] = TEXT("");
    
        // Get a handle to the process.
        HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION |
                                       PROCESS_VM_READ,
                                       FALSE, processID );
    
        // Get the process name.
    
        if (NULL != hProcess )
        {
            HMODULE hMod;
            DWORD cbNeeded;
    
            if ( EnumProcessModules( hProcess, &hMod, sizeof(hMod), &cbNeeded) )
            {
                GetModuleBaseName( hProcess, hMod, szProcessName, sizeof(szProcessName)/sizeof(TCHAR) );
            }
        }
        CloseHandle( hProcess );
        return QString::fromWCharArray(szProcessName);
    }
    
    QStringList WinProcessInfo::listNames(bool removeUnknown)
    {
        QStringList names;
    
        // Get the list of process identifiers.
        DWORD aProcesses[1024], cbNeeded, cProcesses;
        if( !EnumProcesses( aProcesses, sizeof(aProcesses), &cbNeeded )  )
        {
            return names;
        }
        // Calculate how many process identifiers were returned.
        cProcesses = cbNeeded / sizeof(DWORD);
        for (unsigned int i = 0; i < cProcesses; i++ )
        {
            if( aProcesses[i] != 0 )
            {
                QString n = PIDtoName(aProcesses[i]);
                if(!removeUnknown || n != "")
                {
                    names.append(n);
                }
            }
        }
        return names;
    }
    
    QVector WinProcessInfo::listPID()
    {
        QVector pids;
    
        // Get the list of process identifiers.
        DWORD aProcesses[1024], cbNeeded, cProcesses;
        if( !EnumProcesses( aProcesses, sizeof(aProcesses), &cbNeeded )  )
        {
            return pids;
        }
        // Calculate how many process identifiers were returned.
        cProcesses = cbNeeded / sizeof(DWORD);
        for (unsigned int i = 0; i < cProcesses; i++ )
        {
            if( aProcesses[i] != 0 )
            {
                pids.append(aProcesses[i]);
            }
        }
        return pids;
    }
    
    QVector WinProcessInfo::nameToPID(QString name)
    {
        QVector pids;
        // Get the list of process identifiers.
        DWORD aProcesses[1024], cbNeeded, cProcesses;
        if( !EnumProcesses( aProcesses, sizeof(aProcesses), &cbNeeded )  )
        {
            return pids;
        }
        // Calculate how many process identifiers were returned.
        cProcesses = cbNeeded / sizeof(DWORD);
        for (unsigned int i = 0; i < cProcesses; i++ )
        {
            if( aProcesses[i] != 0 )
            {
                if(name == PIDtoName(aProcesses[i]))
                {
                    pids.append(aProcesses[i]);
                }
            }
        }
        return pids;
    }
    
    • 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

    有了这个类,我们就可以判断当前系统中有几个和我们这个程序同名的程序了。

    #include "WinProcessInfo.h"
    #include 
    #include 
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        QString name = QFileInfo(a.applicationFilePath()).fileName();
        if (WinProcessInfo::nameToPID(name).size() > 1)
        {
            QMessageBox::information(0, a.applicationName(), u8"另一个程序实例已经在运行中,不能同时运行两个实例!");
            exit(0);
        }
    
        MainWindow w;
        w.show();
        return a.exec();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当然,这个程序其实是有隐患的。如果我们的电脑上有个别的软件,刚好和我们的软件重名。那么这个判断就是错误的了。所以这个方法我不推荐。

    查找同名的窗口

    这个方法也不能跨平台,下面的代码只针对 WINDOWS 平台。而且如果我们的程序就没有界面,那么这个方法就不适用了。下面是个简单的实现,这个代码还有很多可以优化的地方。这里只是示意性的。

    
    BOOL CALLBACK EnumWindowsProc(
      _In_ HWND   hwnd,
      _In_ LPARAM lParam
    )
    {
        if(lParam == 0) return false;
        QStringList * pList = (QStringList *)lParam;
        TCHAR lpString[256];
        if(::GetWindowText(hwnd, lpString, 255))
        {
            pList->append(QString::fromWCharArray(lpString));
        }
        return true;
    }
    QStringList WinProcessInfo::listWindows()
    {
        QStringList list;
        ::EnumWindows(EnumWindowsProc, (LPARAM)&list);
        return list;
    }
    
    bool WinProcessInfo::findWindow(QString name)
    {
        QStringList list = listWindows();
        return list.contains(name);
    }
    
    • 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

    我们的主程序如下,里面的 XXX 要根据我们的MainWindow 的名字来改:

    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        if (WinProcessInfo::findWindow("XXX"))
        {
            QMessageBox::information(0, a.applicationName(), u8"另一个程序实例已经在运行中,不能同时运行两个实例!");
            exit(0);
        }
    
        MainWindow w;
        w.show();
        return a.exec();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    监控文件

    这种方法也很简单。每次程序运行的时候就生成一个文件。如果这个文件生成成功了。就说明没有其他实例在运行。
    在程序结束之前把这个文件删除掉。不过如果程序中途宕掉了,加锁文件很可能就没删除,导致这个程序无法运行。因此这种方法不推荐。

    下面是个简单的示例:

    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        QFile file(a.applicationDirPath() + "/lock");
        if( !file.open(QIODevice::NewOnly) )
        {
            QMessageBox::information(0, a.applicationName(), u8"另一个程序实例已经在运行中,不能同时运行两个实例!");
            exit(0);
        }
    
        MainWindow w;
        w.show();
    
    
        int ret = a.exec();
        file.remove();
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在网络端口监听

    TCP 或者 UDP 都可以,UDP 比较简单。这个方法的缺点也很明显。首先必须加入 network 组件。
    然后监听的那个端口还要保证其他的程序不会占用。比如下面的程序占用 60000 这个端口。我们只能祈祷这个端口没有其他程序在用。

    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        QUdpSocket socket;
        if (!socket.bind(QHostAddress::LocalHost, 60000))
        {
            QMessageBox::information(0, a.applicationName(), u8"另一个程序实例已经在运行中,不能同时运行两个实例!");
            exit(0);
        }
    
        MainWindow w;
        w.show();
        return a.exec();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    共享内存对象

    这种方法网上的代码最多,不过网上好多代码写的都比较麻烦。基本都是用 attach() 函数来检测是否有其他实例在运行了。如果没有的话再用 create() 建立一个共享内存块。实际上 attach() 是多余的,只要 create() 成功了,就说明没有其他实例在运行。这里我给一个最精简的写法。

    #include 
    #include 
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        QSharedMemory singleton(a.applicationName());
        if (!singleton.create(sizeof(int), QSharedMemory::ReadOnly))
        {
            QMessageBox::information(0, a.applicationName(), u8"另一个程序实例已经在运行中,不能同时运行两个实例!");
            exit(0);
        }
    
        MainWindow w;
        w.show();
        return a.exec();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    博士期间可读的工具书目(含英文原版网盘资源)
    聚量推客滴滴学生认证app地推网推拉新升级啦
    Spring是如何推断构造方法的?
    java注释
    Uniapp软件库源码 全新带勋章功能(包含前后端源码)
    Python爬虫解决中文乱码
    订单超时未支付自动取消8种实现方案
    我的世界村民为什么会消失?
    百度推出可24时直播带货的AI数字人
    【Copilot】登录报错 Extension activation failed: “No auth flow succeeded.“(VSCode)
  • 原文地址:https://blog.csdn.net/liyuanbhu/article/details/126563321