Python - 如何在 Mac OS X 上将 GUI 应用程序转化为守护进程?

5
在Windows上很容易。只需使用pythonw而不是python运行程序,代码将在后台执行。
所以,我想实现的事情很容易安排。
我有一个应用程序,它实际上是一个执行地下工作的服务。但这个服务需要一个控制面板。
因此,在Windows上,我使用wxPython创建GUI,甚至使用一些wx东西来提供所需的服务,当用户完成调整后,她/他点击隐藏并在主窗口上调用Show(False)。
因此,GUI消失了,服务在后台继续工作。用户可以始终使用热键将其带回。
问题是,在Mac OS X上,这种策略只能在某种程度上起作用。
当调用wx.Frame.Show(False)时,窗口和菜单栏消失了,服务正常工作,但应用程序仍然可见。
您可以切换到它,无论您是否可以对其进行任何操作。它仍然存在于Dock等等中。
当程序使用python或pythonw运行或与Py2App捆绑在一起时,就会发生这种情况。
无论我做什么,图标都在那里。
必须有一些技巧允许程序员删除这个淘气的图标,从而在用户不想被打扰时停止打扰可怜的小用户。
隐藏窗口显然不够。 有人知道诀窍吗?
注:我真的很想按照上面描述的方式进行操作,而不是搞乱两个单独的进程和IPC。
编辑:
经过深入挖掘,我找到了这些: 如何从Mac OS X dock隐藏应用程序图标

http://codesorcery.net/2008/02/06/feature-requests-versus-the-right-way-to-do-it

如何隐藏Dock图标

根据上一个链接,正确的方法是使用:

[NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory];

或者

[NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited];

所以我想要的(从后台切换到前台并返回)是可能的。

但如何用Python做到呢?

常量:NSApplicationActivationPolicyProhibited和NSApplicationActivationPolicyAccessory存在于AppKit中,但我找不到任何setApplicationActivationPolicy函数。

NSApp()没有它。

我知道可以通过使用ctypes加载objc dylib、委托给NSApp并发送“setApplicationActivationPolicy:”来实现它,但我不知道这会对wx.App()造成多大的影响。而且对于应该已经可用的东西来说,这有点太麻烦了。

根据我的经验,同时激活NSApp()和wx.App()会互相不喜欢。

也许我们可以以某种方式获取wx正在使用的NSApp()实例并使用wx的委托?

请记住,已经建议使用启动代理并切换到前台或运行多个进程并进行IPC的解决方案在我的情况下非常不可取。

因此,理想情况下,使用setApplicationActivationPolicy是我的目标,但如何做到呢?(简单易行,不要弄乱wx.App()。)

有什么想法吗?


为什么不采用info.plist的解决方案? - Jonah Fleming
因为这意味着进程在后台启动,我想要显示任何GUI都可以,但是一旦失去焦点,我将无法切换回此应用程序,即Dock图标将不会出现。这可以通过使用TransformProcessType()来纠正,但我不知道如何从Python中调用它,而且如果它被实现了,它在哪里隐藏。但是,即使我这样做,这也意味着我必须有两个进程。一个守护进程,保持守护进程状态,和一个GUI,当请求时再次调用应用程序,并告诉它以守护进程运行。 - Dalen
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dalen
2个回答

3

大家好,这里有一个好的、正确的解决方案,没有任何麻烦。

首先,我想解释一下为什么在调用 wx.Frame.Show(MyFrame, False) 后,Windows GUI 进程会进入后台。

简单来说,Windows 认为窗口和应用程序是同一件事。

也就是说,MS Windows 应用程序的主要元素就是您的主 GUI 窗口。

因此,当此窗口被隐藏时,应用程序将没有更多的 GUI 并继续在后台运行。

Mac OS X 则认为应用程序就是您的应用程序,您选择放置在其中的任何窗口都是其子级。

这使您能够在没有窗口但有菜单栏的情况下运行应用程序,从中选择操作,然后生成所需的窗口。

对于编辑器非常方便,您可能同时打开了多个文件,每个文件都在自己的窗口中,当您关闭最后一个文件时,您仍然可以打开一个新文件或创建一个空文件等等。

因此,Mac OS X 应用程序的主要元素是应用程序本身,这就是为什么在隐藏最后一个窗口后它仍然保持打开状态的原因。销毁其菜单栏也无法解决问题。应用程序的名称将在 Dock、应用程序切换器和强制退出中保留。您将能够切换到它并且什么都不能做。 :D

但是,幸运的是,Mac 为我们提供了将其置于后台的函数。这个函数已经在 NSApp 对象的 setApplicationActivationPolicy() 中提到过。

问题在于 Python 的 AppKit 中它的命名是 NSApp.setActivationPolicy_()。更进一步使问题变得复杂的是,它无法直接从 Python 的交互式 shell 中使用,而必须至少从一个导入的模块中调用。

为什么?我不知道。无论如何,下面是一个在 Mac 和 Windows 上将应用程序放入后台的完整示例。

我没有在 Linux 上尝试过它,因为在呈现应用程序方面,Linux 结合了 Mac 和 Windows 的行为,所以是否仅隐藏一个窗口就足够还有待观察。

欢迎尝试并提交编辑,使示例更具跨平台性。

示例:



"""
This app will show you small window with the randomly generated code that will confirm that reopened window is still the same app returned from background,
and the button allowing you to send it to background.
After you send it to background, wait 8 seconds and application will return to foreground again.
Too prove that the application is continuing its work in the background, the app will call wx.Bell() every second.
You should hear the sound while app is in the foreground and when it is in background too.

Merry Christmas and a happy New Year!
"""

import wx
import random, sys

if sys.platform=="darwin":
    from AppKit import NSBundle, NSApp, NSAutoreleasePool, NSApplicationActivationPolicyRegular, NSApplicationActivationPolicyProhibited

    # Use Info.plist values to know whether our process started as daemon
    # Also, change this dict in case anyone is later checking it (e.g. some module)
    # Note: Changing this dict doesn't change Info.plist file
    info = NSBundle.mainBundle().infoDictionary()

    def SendToBackground ():
        # Change info, just in case someone checks it later
        info["LSUIElement"] = "1"
        NSApp.setActivationPolicy_(NSApplicationActivationPolicyProhibited)

    def ReturnToForeground ():
        # Change info, just in case someone checks it later
        info["LSUIElement"] = "0"
        NSApp.setActivationPolicy_(NSApplicationActivationPolicyRegular)

else:
    # Simulate Mac OS X App - Info.plist
    info = {"LSUIElement": "0"} # Assume non background at startup
                                # If programmer chose not to display GUI at startup then she/he should change this before calling ReturnToForeground()
                                # To preserve consistency and allow correct IsDaemon() answer
    def SendToBackground ():
        info["LSUIElement"] = "1"

    def ReturnToForeground ():
        info["LSUIElement"] = "0"

def IsDaemon ():
    return info["LSUIElement"]=="1"

class Interface (wx.Frame):
    def __init__ (self):
        wx.Frame.__init__(self, None, -1, "Test", pos=(100, 100), size=(100, 100))
        wx.StaticText(self, -1, "Test code: "+str(random.randint(1000, 10000)), pos=(10, 10), size=(80, 20))
        b = wx.Button(self, -1, "DAEMONIZE ME", size=(80, 20), pos=(10, 50))
        wx.EVT_BUTTON(self, b.GetId(), self.OnDaemonize)
        self.belltimer = wx.Timer(self)
        wx.EVT_TIMER(self, self.belltimer.GetId(), self.OnBellTimer)
        self.belltimer.Start(1000)
        # On Mac OS X, you wouldn't be able to quit the app without the menu bar:
        if sys.platform=="darwin":
            self.SetMenuBar(wx.MenuBar())
        self.Show()

    def OnBellTimer (self, e):
        wx.Bell()

    def OnDaemonize (self, e):
        self.Show(False)
        SendToBackground()
        self.timer = wx.Timer(self)
        wx.EVT_TIMER(self, self.timer.GetId(), self.OnExorcize)
        self.timer.Start(8000)

    def OnExorcize (self, e):
        self.timer.Stop()
        ReturnToForeground()
        self.Show()
        self.Raise()

app = wx.App()
i = Interface()
app.MainLoop()

当然,这个例子可以从终端或CLI窗口开始。在这种情况下,终端对程序的控制将保持打开状态,而应用程序将只是出现和消失。

要完成GUI守护进程,您应该使用pythonw(在Windows上)启动它,或从daemontest.pyw文件中启动它,在Mac上您应该使用:

% nohup python daemontest.py &

或者将其与py2app捆绑在一起,或使用python.org Python版本附带的Python启动器启动daemontest.py而无需终端。

注意:此示例在Mac OS X上存在与我在问题中提供的链接中提到的相同缺陷。我指的是当应用程序从后台返回时,焦点错误和菜单栏不立即出现的问题。用户必须切换并重新回到新返回的应用程序才能正常工作。我希望有人也能解决这个问题。尽快。这非常烦人。

还有一点需要注意:如果您的程序中有正在运行的线程,请在daemonizing和exorcizing时暂停它们。特别是如果它们正在使用Apple事件与另一个应用程序通信。坦白地说,还应该对wx.Timers进行一些操作。如果不小心,您可能会在程序终止时遇到围绕不存在的NSAutoreleasePool和/或SegmentationFault的泄漏问题。


非常详细和有帮助! - Jonah Fleming

0

好的。这里是实现你想要做的事情的代码:

import AppKit
info = AppKit.NSBundle.mainBundle().infoDictionary()
info["LSUIElement"] = "1"

这是一个比较麻烦的答案,但我还是会列出来。在info.plist文件中添加以下键:
<key>LSUIElement</key>
<string>1</string>

另一种更加守护进程的解决方案,但这意味着它不能有图形用户界面,您需要将此键添加到info.plist文件中:

<key>LSBackgroundOnly</key>
<string>1</string>

来源


当然不会!在wx.App()或任何其他App类初始化之前,操作此字典中的值可能只有影响。wx.App()从中获取值,如果LSUIElement = 1,则应用程序不会呈现给用户。 - Dalen
然而,你竟然尝试给出了一个答案。如果问题中的dict()不是一个dict(),而是一个类似于dict的对象,它可以将更改计入计数。那么更改值将触发显示/隐藏。所以为此尝试表示赞扬。此外,你的答案在我疯狂搜索时帮助我找到了解决方案。请看我的答案。我还使用了你的A作为如何完成一些工作的建议,因此我认为你非常值得获得+50奖励。谢谢! - Dalen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接