在我的Python Qt应用程序中,如何将Mac OSX应用程序菜单栏项设置为除“Python”以外的其他内容?

23

我正在使用Python和Qt编写GUI应用程序。在Mac上启动应用程序时,屏幕顶部的Mac菜单栏中第一个菜单项是“Python”。我想要显示我的应用程序名称而不是“Python”。如何将我的程序名称添加到菜单栏中呢?

以下演示程序创建了一个带有两个菜单的窗口:"Python"和"Foo"。我不喜欢这样,因为对于我的用户来说,我是使用Python还是COBOL编写的应用程序并没有区别。相反,我想要显示“我的应用程序”和“Foo”菜单。

#!/usr/bin/python

# This example demonstrates unwanted "Python"
# application menu name on Mac.

# Makes no difference whether we use PySide or PyQt4
from PySide.QtGui import *
# from PyQt4.QtGui import *

import sys

app = QApplication(sys.argv)
# Mac menubar application menu is always "Python".
# I want "DesiredAppTitle" instead.
# setApplicationName() does not affect Mac menu bar.
app.setApplicationName("DesiredAppTitle")
win = QMainWindow()
# need None parent for menubar on Mac to get custom menus at all
mbar = QMenuBar()
# Add a custom menu to menubar.
fooMenu = QMenu(mbar)
fooMenu.setTitle("Foo")
mbar.addAction(fooMenu.menuAction())
win.setMenuBar(mbar)
win.show()
sys.exit(app.exec_())

我该如何在Mac上更改应用程序菜单名称?编辑:如果可能的话,我希望继续使用系统Python(或用户PATH上的任何Python)。


你是如何启动程序的?我对Mac了解甚少,但听起来好像只是将sys.argv [0]的值放在那里。 - Falmarri
我通过输入“python MyApp.py”或“./MyApp.py”来启动程序。在这两种情况下,sys.argv[0]都是“MyApp.py”或“./MyApp.py”。 “Python”不是argv的一部分。 - Christopher Bruns
@Christpher Bruns:嗯,你说得对。我不知道为什么我认为Python的argv会将Python作为它的argv[0]。虽然我对Mac一无所知,但我已经没有更多的想法了。 - Falmarri
6个回答

9
您需要一个OSX .app才能使此功能正常工作,因为其中的Info.plist文件包含放置在那里的应用程序的用户可见名称。默认值为Python,这是您在程序菜单中看到的标题。此博客文章概述了您需要执行的步骤,而OSX开发人员文库有关于需要填充的属性列表的文档。

如果必要的话,我愿意创建一个App捆绑包。但是我对那篇博客文章中“你不能使用系统Python”的结论感到非常不满。难道真的没有办法使用系统Python而不出现“Python”菜单项吗? - Christopher Bruns
显然...引用另一个来源的话:“在构建基于Python的依赖项之前,绝对必要配置环境以使用正确版本的Python。”从我记得的某个任务中,这种方法肯定有效,一旦我们自动化了部署,这就不是问题了。我不知道有什么其他方法,所以...我想没有其他方法了。 - jro
我找到了一种方法。请看我的答案。如果您认为可以将其转化为更完整的解决方案,我鼓励您开始第二个答案。 - Christopher Bruns

5
如果您打算分发应用程序,则符号链接Python二进制文件不能保证有效,因为通常在开发机器上系统Python不是默认的Python,并且普通用户很可能没有安装Qt和PyQt。更可靠的方法是拥有一个本地的OSX引导二进制文件,该文件将负责启动您的PyQt应用程序。 py2appPyInstaller 都可以生成本地包装器。虽然py2app 可以很好地创建一个别名应用程序,即一个符号链接到系统上现有文件的包装器,但使其仅捆绑所需的PyQt依赖项则被证明是棘手的。此外,使用--aliased标志由py2app 生成的“别名应用程序”如果移动到另一个文件夹,则将停止工作,因为符号链接相对于最初运行构建脚本的文件夹。

PyInstaller

PyInstaller 开箱即用,我最终得到了一个大小约为16MB的包含所有依赖项的OSX捆绑包。

将Python脚本打包成独立的OSX应用程序:

pyinstaller -w --noconfirm -i=myappicon.icns --clean -F myscript.py

这将生成一个独立的捆绑包,该包将在OSX标题栏中显示您在.setWindowTitle()中设置的任何内容作为应用程序名称。-w开关非常重要,因为它将创建一个带有适当的.plist文件的MacOS应用程序捆绑包。
注意:由于某些原因,通过pip安装的pyinstaller版本对我无效。因此,我卸载了原始的pip版本(pip uninstall pyinstaller),并使用以下命令从github的develop分支安装了最新版。
pip install -v -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=pyinstaller-github

然后它像魔法一样工作。

Mac Automator

另一个创建应用程序包装器的选项是使用Automator(在Spotlight中键入名称),转到文件 > 新建 > 应用程序 > 搜索并拖动Run shell script到编辑器中,在Shell下选择bashpython。您还可以使用运行AppleScript和引导您的应用程序使用AppleScript - 如果您需要请求用户密码以便以升级特权运行,则可能非常有用。

Automator 的一个更好的替代品是免费开源的应用Platypus,它将各种类型的脚本封装到OSX应用程序中,并提供了一些额外的功能,如GUI文本输出窗口、以管理员权限运行、设置自定义图标等。代码可在github上获得。

还可以在Xcode中创建一个简单的OSX应用程序,该应用程序将启动您的PyQt脚本(一些示例在这里),并执行应用程序所需的其他自定义任务。

注意:请记住,调用您的Python代码的应用程序包是一个“浅层”应用程序包,将依赖于您本地安装的任何Python依赖项。最可能不会在别人的Mac上开箱即用。

Nuitka

您还可以尝试Nuitkapip3 install nuitka),它旨在通过将您的Python代码转换为C++,然后编译它来构建真正的本地可执行文件的项目。

使用nuitka构建本机应用程序的示例:

nuitka3 --follow-imports --python-flag=no_site --verbose --standalone --show-progress --show-modules --output-dir=my_build_dir myscript.py

这将在my_build_dir文件夹中创建带有库的Mac可执行文件。您需要自己使用MacOS应用程序捆绑包将它们包装起来,例如使用PlatypusAutomator


5
我已经找到了这个问题的答案,因为我想把奖励授予不是我自己(即楼主)的人,请任何人接受这个答案,并将其完善成更完整的答案。
我可以通过以下方式将应用程序菜单设置为“我的应用程序”:
ln -s /System/Library/Frameworks/Python.framework/Versions/2.6/Resources/Python.app/Contents/MacOS/Python MyApp

./MyApp MyApp.py

要使这个工作,需要两个元素:

  1. 符号链接必须命名为“MyApp”(或者您希望在应用程序菜单中显示的任何名称)
  2. 符号链接必须指向系统 Python 应用程序包内部的 Python 可执行文件。如果您链接到 /usr/bin/python,它将无法正常工作。

一定有一个聪明的方法可以创建一个应用程序包或 shell 脚本,以稳健的方式利用这种机制...


如果我理解正确的话,你只是在更改可执行文件的名称,是这样吗(我了解符号链接,但需要澄清)?虽然我仍然支持创建一个应用程序包并将自动化工作放在其中,但你可以简单地为你的应用程序创建一个启动器,首先创建符号链接,启动你的应用程序,并在关闭它后删除该链接。但正如我所说,我更支持创建一个适当的应用程序包... 所以有人可以随意将其编译成自己的答案。 - jro
@jro 我同意使用应用程序包可能需要优雅地实现符号链接策略。我正在寻找一种解决方案,展示如何在应用程序包中创建和使用符号链接。如果没有更具体的答案出现,你的“使用应用程序包”答案可能会成为最佳选择。 - Christopher Bruns

2

我知道这不完全是提问者想要的,但认为这可能会增加寻找解决方案的价值。

您可以将文件菜单显示在窗口界面中,而不是在原生的Mac OS X系统菜单栏中:

menubar.setNativeMenuBar(False)

这将使菜单栏的显示与在Windows系统中相同。


1

我希望最终用户可以访问我的Python代码,这样他们就可以看到并可能改进代码,因此我不感兴趣隐藏代码的选项。我发现我可以使用不同的标准Python解释器将wx应用程序(尚未尝试使用Qt)包装成捆绑包,并在安装它们时(例如,作为conda软件包安装过程的一部分)以指定的应用程序名称运行。以下是一个简短的脚本,使用执行脚本的Python创建具有指定名称的.app包。使用创建的软链接(请参见下面的pyAlias)运行脚本,如下所示:

./MyApplication.app/Contents/MacOS/MyApplication /path/MyApplication.py 

这份工作做得如何。

from __future__ import division, print_function
import os,sys
appName = 'MyApplication'    # name of app
scriptdir = '.'
iconfile = os.path.join(scriptdir,'MyApplication.icns') # optional icon file

if __name__ == '__main__':
    wrapApp = os.path.join(scriptdir,appName+'.app','Contents')
    os.makedirs(wrapApp)
    wrapPy = os.path.join(wrapApp,'MacOS')
    os.makedirs(wrapPy)
    fp = open(os.path.join(wrapApp,'PkgInfo'),'w')
    fp.write('APPL????\n')
    fp.close()
    fp = open(os.path.join(wrapApp,'Info.plist'),'w')
    fp.write('''<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">\n<plist version="0.9">\n<dict>\n        <key>CFBundleIconFile</key>\n        <string></string>\n        <key>CFBundlePackageType</key>\n        <string>APPL</string>\n        <key>CFBundleGetInfoString</key>\n        <string>Created in makeApp.py (Brian Toby/QMake</string>\n        <key>CFBundleSignature</key>\n        <string>????</string>\n        <key>CFBundleExecutable</key>\n        <string>{:}</string>\n        <key>CFBundleIdentifier</key>\n        <string>com.continuum.python</string>\n        <key>NSPrincipalClass</key>\n        <string>NSApplication</string>\n</dict>\n</plist>\n'''.format(appName))
    fp.close()
    pyAlias = os.path.join(wrapPy,appName)
    pythonpath = os.path.realpath(sys.executable)
    os.symlink(pythonpath,pyAlias)
    if os.path.exists(iconfile):
        shutil.copyfile(iconfile,oldicon)
    print('Use',pyAlias,'to run wxPython scripts')

以下是一篇较旧的答案,我保留它以供历史参考。在我的应用程序中,我使用一种混合方法,在其中创建了一个拖放的AppleScript捆绑包(如下所示),在其中放置了一个Python的软链接(命名为与我的应用程序相匹配的名称)。
在Christopher Bruns的答案基础上,以及从“如何通过Python创建Mac应用程序包”的脚本,这是一个Python脚本,用于为用户Python脚本创建一个bundle(应用程序),它将在菜单中显示应用程序名称而不是“Python”。为此,它尝试查找已打包的Python版本,并使用应用程序的名称创建符号链接。我用wxpython脚本测试了它,但对Qt也应该适用。
用户脚本是从其原始位置运行的,而不是将其放在应用程序中。如果您想将自己的脚本(以及Python)放入捆绑包中以进行重新分发,请参见py2app
#!/usr/bin/env python
'''This creates an app to launch a python script. The app is
created in the directory where python is called. A version of Python
is created via a softlink, named to match the app, which means that
the name of the app rather than Python shows up as the name in the
menu bar, etc, but this requires locating an app version of Python
(expected name .../Resources/Python.app/Contents/MacOS/Python in
directory tree of calling python interpreter).

Run this script with one or two arguments:
    <python script>
    <project name>
The script path may be specified relative to the current path or given
an absolute path, but will be accessed via an absolute path. If the
project name is not specified, it will be taken from the root name of
the script.
'''
import sys, os, os.path, stat
def Usage():
    print("\n\tUsage: python "+sys.argv[0]+" <python script> [<project name>]\n")
    sys.exit()

version = "1.0.0"
bundleIdentifier = "org.test.test"

if not 2 <= len(sys.argv) <= 3:
    Usage()

script = os.path.abspath(sys.argv[1])
if not os.path.exists(script):
    print("\nFile "+script+" not found")
    Usage()
if os.path.splitext(script)[1].lower() != '.py':
    print("\nScript "+script+" does not have extension .py")
    Usage()

if len(sys.argv) == 3:
    project = sys.argv[2]
else:
    project = os.path.splitext(os.path.split(script)[1])[0]

# find the python application; must be an OS X app
pythonpath,top = os.path.split(os.path.realpath(sys.executable))
while top:
    if 'Resources' in pythonpath:
        pass
    elif os.path.exists(os.path.join(pythonpath,'Resources')):
        break
    pythonpath,top = os.path.split(pythonpath)
else:
    print("\nSorry, failed to find a Resources directory associated with "+str(sys.executable))
    sys.exit()
pythonapp = os.path.join(pythonpath,'Resources','Python.app','Contents','MacOS','Python')
if not os.path.exists(pythonapp): 
    print("\nSorry, failed to find a Python app in "+str(pythonapp))
    sys.exit()

apppath = os.path.abspath(os.path.join('.',project+".app"))
newpython =  os.path.join(apppath,"Contents","MacOS",project)
projectversion = project + " " + version
if os.path.exists(apppath):
    print("\nSorry, an app named "+project+" exists in this location ("+str(apppath)+")")
    sys.exit()

os.makedirs(os.path.join(apppath,"Contents","MacOS"))

f = open(os.path.join(apppath,"Contents","Info.plist"), "w")
f.write('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>main.sh</string>
    <key>CFBundleGetInfoString</key>
    <string>{:}</string>
    <key>CFBundleIconFile</key>
    <string>app.icns</string>
    <key>CFBundleIdentifier</key>
    <string>{:}</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>{:}</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>{:}</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>{:}</string>
    <key>NSAppleScriptEnabled</key>
    <string>YES</string>
    <key>NSMainNibFile</key>
    <string>MainMenu</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
</dict>
</plist>
'''.format(projectversion, bundleIdentifier, project, projectversion, version)
    )
f.close()

# not sure what this file does
f = open(os.path.join(apppath,'Contents','PkgInfo'), "w")
f.write("APPL????")
f.close()
# create a link to the python app, but named to match the project
os.symlink(pythonapp,newpython)
# create a script that launches python with the requested app
shell = os.path.join(apppath,"Contents","MacOS","main.sh")
# create a short shell script
f = open(shell, "w")
f.write('#!/bin/sh\nexec "'+newpython+'" "'+script+'"\n')
f.close()
os.chmod(shell, os.stat(shell).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

-3

我认为你需要这样做:

win.setWindowTitle("MyApp")

编辑:这是针对 PyQt 的。我不知道 PySide 中的等效物。


2
setWindowTitle() 将文本放置在应用程序标题栏中,而不是菜单栏中。 我需要更改 Mac 菜单栏中的应用程序菜单项。 setWindowTitle() 无法实现此功能。 - Christopher Bruns

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