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

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

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

* 查找系统的进程列表,看看有没有同名的进程已经运行了。有的话说明有其他实例在运行。
* 查找有没同名的窗体。有的话说明有其他实例在运行。
另一种思路是在程序中创造一种条件,这个条件可以被其他的实例感知。比如:

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

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

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

这个类的头文件如下:
#ifndef WINPROCESSINFO_H #define WINPROCESSINFO_H #include <Windows.h>
#include <Psapi.h> #include <QStringList> #include <QVector> class
WinProcessInfo { public: WinProcessInfo(); static QString PIDtoName(DWORD pid);
static QStringList listNames(bool removeUnknown = true); static QVector<DWORD>
listPID(); static QVector<DWORD> nameToPID(QString name); }; #endif //
WINPROCESSINFO_H
类的实现文件如下:
#include "WinProcessInfo.h" WinProcessInfo::WinProcessInfo() { } QString
WinProcessInfo::PIDtoName(DWORD processID) { TCHAR szProcessName[MAX_PATH] =
TEXT("<unknown>"); // 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 != "<unknown>") { names.append(n); } } } return names; } QVector<DWORD>
WinProcessInfo::listPID() { QVector<DWORD> 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<DWORD>
WinProcessInfo::nameToPID(QString name) { QVector<DWORD> 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; }
有了这个类,我们就可以判断当前系统中有几个和我们这个程序同名的程序了。
#include "WinProcessInfo.h" #include <QFileInfo> #include <QMessageBox> 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(); }
当然,这个程序其实是有隐患的。如果我们的电脑上有个别的软件,刚好和我们的软件重名。那么这个判断就是错误的了。所以这个方法我不推荐。

<>查找同名的窗口

这个方法也不能跨平台,下面的代码只针对 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); }
我们的主程序如下,里面的 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(); }
<>监控文件

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

下面是个简单的示例:
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; }
<>在网络端口监听

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(); }
<>共享内存对象

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

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信