最近有个小项目,客户要求程序只能运行一个实例。以前没遇到过这种要求,这次特意花了点时间研究了一下。
大概想了一下,有两种思路。一种是直接去找这个程序已经运行的线索。比如:
另一种思路是在程序中创造一种条件,这个条件可以被其他的实例感知。比如:
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
类的实现文件如下:
#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;
}
有了这个类,我们就可以判断当前系统中有几个和我们这个程序同名的程序了。
#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();
}
当然,这个程序其实是有隐患的。如果我们的电脑上有个别的软件,刚好和我们的软件重名。那么这个判断就是错误的了。所以这个方法我不推荐。
这个方法也不能跨平台,下面的代码只针对 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
#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();
}