用Qt开发Mac App超过5年时间,之前也单独用Cocoa上架了一款Mac软件,但如果考虑的是跨平台Win和Linux的需求,基本还是用Qt为主。Qt定制Mac下有几点是不得不考虑的:
1. 接入sqlite。必须用纯C的写法,QSqlDatabase那套API,苹果Mac App Store审核直接以私有api拒绝了.
int result = sqlite3_open(ch, &database);
sqlite3_exec(database, "PRAGMA synchronous = OFF; ", 0, 0, 0);
int ret = sqlite3_close(database);
int result = sqlite3_prepare_v2(database, sql, -1, &stmt, NULL);
2. 内嵌Web页面。浏览器只能用WKWebview,不能使用任何非Webkit内核的浏览器,比如CEF,WebEngine。同样会被苹果审核以使用私有api拒绝。
MacWebView *cWebView = [[[MacWebView alloc] initWithFrame:rect configuration:config target:this] autorelease];
webViewHandler = (WId)cWebView;
QWindow *webviewWin = QWindow::fromWinId(webViewHandler);
QWidget *webviewContainer = QWidget::createWindowContainer(webviewWin, this);
// 剩下操作都在WKNavigationDelegate和WKUIDelegate的回调中处理
@interface MacWebView: WKWebView {
QPointer<WebViewInMac> pTarget;
}
3. 系统皮肤设置。苹果有浅色,深色和自动三种皮肤模式。Qt没有提供代码,只能使用苹果官方语言开发,代码如下:
QString DeviceInfo::getAppearanceName()
{
NSAppearanceName appearName;
if ([NSAppearance respondsToSelector:@selector(currentDrawingAppearance)])
{
appearName = NSAppearance.currentDrawingAppearance.name;
}
else
{
appearName = NSAppearance.currentAppearance.name;
}
if (NSAppearanceNameAqua == appearName)
{
return SKIN_WHITE;
}
else if (NSAppearanceNameDarkAqua == appearName)
{
return SKIN_BLACK;
}
return "";
}
void DeviceInfo::setAppearance(const QString& name)
{
if (SKIN_WHITE == name)
NSApp.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
else if (SKIN_BLACK == name)
NSApp.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
else
NSApp.appearance = nil;
}
然后在MainWindow的changeEvent事件回调等待系统触发的皮肤切换事件
void MainWindow::changeEvent(QEvent *event)
{
#if defined(Q_OS_DARWIN)
if (event->type() == QEvent::ThemeChange) // 注意改成ThemeChange,不是PaletteChange
{
// 需要针对自动模式特殊处理
QString appearName = DeviceInfo::getAppearanceName();
}
}
4. 窗口标题栏定制,设置高度,存放按钮,修改样式等。
// 设置窗体无边框
MainWindow w;
w.setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
NSView *view = (NSView *) w->winId();
NSWindow *window = [view window];
self.cocoaWin = window;
window.titlebarAppearsTransparent = YES;
window.titleVisibility = NSWindowTitleHidden;
window.backgroundColor = [NSColor colorWithRed:(float)53/255 green:(float)61/255 blue:(float)71/255 alpha:1.];
window.styleMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
| NSWindowStyleMaskResizable | NSWindowStyleMaskTitled
| NSWindowStyleMaskFullSizeContentView;
window.opaque = NO;
window.delegate = self;
// 剩下就是新增NSView,NSButton的工作
//这里还需要注意NSWindow的回调
- (void) windowDidExitFullScreen:(NSNotification *)notification
{
NSInteger major, minor;
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
major = processInfo.operatingSystemVersion.majorVersion;
minor = processInfo.operatingSystemVersion.minorVersion;
NSWindow *window = notification.object;
if ((major == 10 && minor < 14) || major < 10)
window.styleMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
| NSWindowStyleMaskResizable | NSWindowStyleMaskTitled;
else
window.styleMask = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable
| NSWindowStyleMaskResizable | NSWindowStyleMaskTitled
| NSWindowStyleMaskFullSizeContentView;
}
- (void) windowDidResize:(NSNotification *)notification
{
}
5. Dock图标的右键退出功能实现,同样需要苹果官方语言开发
void action_showMain()
{
foreach(QWidget* w, ((QApplication*)qApp)->topLevelWidgets())
{
if (MainWindow* mainWin = qobject_cast<MainWindow*>(w))
{
if (mainWin->isMinimized())
{
mainWin->showMaximized();
}
}
else if (WebPanel* mainWin = qobject_cast<WebPanel*>(w))
{
if (mainWin->isMinimized())
{
mainWin->showNormal();
}
}
}
}
bool applicationShouldHandleReopen(id self,SEL _cmd, ...)
{
// 点击dock按钮时调用方法
action_showMain();
return false;
}
需要在main函数中启动方法替换
void setupDockClickEvent()
{
Class cls = objc_getClass("NSApplication");
id appInstance = ((id(*)(Class,SEL))objc_msgSend)(cls, sel_registerName("sharedApplication"));
if(appInstance != nullptr)
{
id appDelegate = ((id(*)(Class,SEL))objc_msgSend)(appInstance, sel_registerName("delegate"));
Class delClass =((id(*)(Class,SEL))objc_msgSend)(appDelegate, sel_registerName("class"));
SEL shouldHandle = sel_registerName("applicationShouldHandleReopen:hasVisibleWindows:");
if (class_getInstanceMethod(delClass, shouldHandle))
{
// 使用Swizzling黑魔法替换 applicationShouldHandleReopen:hasVisibleWindows: 系统方法
if (class_replaceMethod(delClass, shouldHandle, reinterpret_cast<IMP>(applicationShouldHandleReopen), "B@:"))
qDebug() << "applicationShouldHandleReopen方法替换成功";
else
qDebug() << "applicationShouldHandleReopen方法替换失败";
}
else
{
if (class_addMethod(delClass, shouldHandle, reinterpret_cast<IMP>(applicationShouldHandleReopen),"B@:"))
qDebug() << "applicationShouldHandleReopen添加dock点击方法成功";
else
qDebug() << "applicationShouldHandleReopen添加dock点击方法成功失败";
}
SEL shouldTerminate = sel_registerName("applicationShouldTerminate:");
if (class_getInstanceMethod(delClass, shouldTerminate))
{
if (class_replaceMethod(delClass, shouldTerminate, reinterpret_cast<IMP>(applicationShouldTerminate), "B@:"))
qDebug() << "applicationShouldTerminate方法替换成功";
else
qDebug() << "applicationShouldTerminate方法替换失败";
}
else
{
if (class_addMethod(delClass, shouldTerminate, reinterpret_cast<IMP>(applicationShouldTerminate),"B@:"))
qDebug() << "applicationShouldTerminate添加dock点击方法成功";
else
qDebug() << "applicationShouldTerminate添加dock点击方法成功失败";
}
}
}
6. Ping处理,因Mac平台下断线重连比Win下要慢很多,故采用了PING服务器IP进行快速重连操作,这里封装了SimplePing的开源代码,故此不再展示。