Python制作托盘工具,还能控制系统音乐播放?
前情
用电脑的时候,我一直羡慕苹果的一个功能:
是的,你可以随时随地控制音乐。
有一天我想暂停一下歌曲,但是正好我把音乐软件最小化了,于是我要先打开它,然后点暂停。
然后我再也受不了了,我准备写一个能在托盘打开,然后快速控制音乐的工具。
后来我查了很多资料,终于我在Stack Overflow上找到一些头绪,我将在这篇随笔中分享整个软件制作过程。
探索和拆分的代码
这部分的代码你拼起来可能也跑不了,我不对这个主题下的代码做能跑保证,所以你可以看看完整代码。
歌曲名显示
这部分其实不难,给予我灵感的那篇帖子已经将这作为例子,直接贴出了代码。我通过这个帖子找到了一个叫做winrt的库和它的文档,这个库看上去十分强大,似乎可以直接调用UWP的API。
我将关于获取媒体信息的这部分代码这部分代码几乎直接照抄到了我的程序里。
async def get_media_name(): sessions = await MediaManager.request_async() # This source_app_user_model_id check and if statement is optional # Use it if you want to only get a certain player/program's media # (e.g. only chrome.exe's media not any other program's). # To get the ID, use a breakpoint() to run sessions.get_current_session() # while the media you want to get is playing. # Then set TARGET_ID to the string this call returns. current_session = sessions.get_current_session() if current_session: # there needs to be a media session running #if current_session.source_app_user_model_id == TARGET_ID: info = await current_session.try_get_media_properties_async() # song_attr[0] != '_' ignores system attributes info_dict = {song_attr: info.__getattribute__(song_attr) for song_attr in dir(info) if song_attr[0] != '_'} # converts winrt vector to list info_dict['genres'] = list(info_dict['genres']) name=info_dict['title'] if info_dict['artist']!='': artist=info_dict['artist'] else: artist='未知歌手' if info_dict['album_title']!='': artist+=' — '+info_dict['album_title'] else: artist+=' — '+'未知专辑' return name,artist else: return '未在播放','N / A' # It could be possible to select a program from a list of current # available ones. I just haven't implemented this here for my use case. # See references for more information. raise Exception('歌曲信息获取函数未按计划结束') async def get_media_info(): sessions = await MediaManager.request_async() # This source_app_user_model_id check and if statement is optional # Use it if you want to only get a certain player/program's media # (e.g. only chrome.exe's media not any other program's). # To get the ID, use a breakpoint() to run sessions.get_current_session() # while the media you want to get is playing. # Then set TARGET_ID to the string this call returns. current_session = sessions.get_current_session() if current_session: # there needs to be a media session running #if current_session.source_app_user_model_id == TARGET_ID: info = await current_session.try_get_media_properties_async() # song_attr[0] != '_' ignores system attributes info_dict = {song_attr: info.__getattribute__(song_attr) for song_attr in dir(info) if song_attr[0] != '_'} # converts winrt vector to list info_dict['genres'] = list(info_dict['genres']) return info_dict else: return { 'album_artist': '', 'album_title': '', 'album_track_count': 0, 'artist': '', 'genres': [], 'playback_type': 0, 'subtitle': '', 'thumbnail': 'NO_DATA', 'title': '', 'track_number': 0,} # It could be possible to select a program from a list of current # available ones. I just haven't implemented this here for my use case. # See references for more information. raise Exception('歌曲信息获取函数未按计划结束')
这两个函数,一个用于获取完整信息,一个用于获取歌曲名、专辑、艺人三个信息,其中艺人与专辑合并在一个字符串中,它们在软件界面上也像这样显示在一起。
此外还有个并不起眼而且不知道干啥用的函数:
async def read_stream_into_buffer(stream_ref, buffer): readable_stream = await stream_ref.open_read_async() readable_stream.read_async(buffer, buffer.capacity, InputStreamOptions.READ_AHEAD)
至于async是什么,请不要问我,因为我(们?)并不且没必要知道。
歌曲控制
帖子作者说,TA并不需要写出这些功能,所以TA只留了些提示,我只好自己探索了。
相关内容译文
控制媒体
正如楼主所说,他们的最终目标是控制媒体,在相同的库中,这是可能实现的。这里可能有更多信息(在我的案例中,我不需要这些):
微软 WinRT 文档 - Windows.Media.Control - GlobalSystemMediaTranportSession类(例.await current_session.try_pause_async())
以上是自己翻译的,可能不准。
根据大佬的指示,我直接查阅了链接指向的文档Microsoft WinRT Docs - Windows.Media.Control - GlobalSystemMediaTransportControlsSession class。
打开文档后,只需要像这样简单地改改链接就能看中文版了。
我整理了一下对我来说比较有用的方法(咱也不懂,咱也不用懂,我只是希望我能解释得通俗些,懂的大佬不要喷,有话好说,谢谢):
然后我直接修改了前面获取歌曲信息的代码,写出了常用的控制函数
async def stop(): sessions = await MediaManager.request_async() current_session = sessions.get_current_session() info = await current_session.try_toggle_play_pause_async() async def nextm(): sessions = await MediaManager.request_async() current_session = sessions.get_current_session() info = await current_session.try_skip_next_async() async def prem(): sessions = await MediaManager.request_async() current_session = sessions.get_current_session() info = await current_session.try_skip_previous_async()
界面
是的,我用tkinter已经用魔怔了……
模仿着iOS的音乐控制界面和Win10的界面风格,我以自己和tkinter的水平做了个布局差不多,但是功能差很多的界面,然后还整了个托盘图标。
#界面 win=tk.Tk() win.title('音乐控制') win.resizable(0,0) win.protocol('WM_DELETE_WINDOW',win.withdraw) win.attributes("-toolwindow", True) imgf=Image.open("media_thumb_none.jpg") imgf = imgf.resize((250, 250)) img=ImageTk.PhotoImage(imgf) imgt=tk.Label(win,image=img) imgt.pack() nametxt=tk.Label(win,text='加载中',font=('等线',20)) nametxt.pack() artxt=tk.Label(win,text='加载中',font=('等线',12),fg='#909090') artxt.pack() btnpt=tk.Frame(win) btnpt.pack(fill=tk.X) prembtn=tk.Button(btnpt,text='9',command=lambda:asyncio.run(prem()),font=('webdings',25),bd=0) prembtn.pack(side=tk.LEFT,fill=tk.X) nextmbtn=tk.Button(btnpt,text=':',command=lambda:asyncio.run(nextm()),font=('webdings',25),bd=0) nextmbtn.pack(side=tk.RIGHT,fill=tk.X) stopbtn=tk.Button(btnpt,text='4‖',command=lambda:asyncio.run(stop()),font=('webdings',25),bd=0,bg='#A6D8FF') stopbtn.pack(fill=tk.X) #获取窗口默认大小 win.update() winw=win.winfo_width() winh=win.winfo_height() #屏幕尺寸 scr_w=win32api.GetSystemMetrics(win32con.SM_CXSCREEN) scr_h=win32api.GetSystemMetrics(win32con.SM_CYSCREEN) #窗口大小与位置 win.geometry(str(winw)+'x'+str(winh)+'+'+str(int(scr_w-winw-10))+'+'+str(int(scr_h-winh-75))) #先进行一次外观刷新 if get_dark(): win.configure(background='#101010') nametxt['bg']='#101010' nametxt['fg']='#FFFFFF' artxt['bg']='#101010' #artxt['fg']='#FFFFFF' imgt['bg']='#101010' prembtn['bg']='#101010' prembtn['fg']='#FFFFFF' nextmbtn['bg']='#101010' nextmbtn['fg']='#FFFFFF' stopbtn['bg']='#0078D7' stopbtn['fg']='#FFFFFF' else: win.configure(background='#FFFFFF') nametxt['bg']='#FFFFFF' nametxt['fg']='#000000' artxt['bg']='#FFFFFF' #artxt['fg']='#000000' imgt['bg']='#FFFFFF' prembtn['bg']='#FFFFFF' prembtn['fg']='#000000' nextmbtn['bg']='#FFFFFF' nextmbtn['fg']='#000000' stopbtn['bg']='#A6D8FF' stopbtn['fg']='#000000' rft=0#ReFresh Time refresh_t=threading.Thread(target=refresh) refresh_t.start() win.withdraw() #托盘 use_color_icon=False menu = (pystray.MenuItem('显示音乐控制中心',show_window,default=True),pystray.MenuItem('退出',close),pystray.MenuItem('使用彩色托盘图标',change_icon,checked=lambda item: use_color_icon)) iconimg = Image.open("icon.png") icon = pystray.Icon("PyMusicCtrl",iconimg,"音乐控制",menu) icon.run_detached() win.mainloop()
实时更新歌曲信息
首先一个while True,然后套进各种信息的刷新。
大部分就是直接修改界面上各种控件的属性
获取专辑图
我也不知道为啥这个API不直接把专辑图放在属性里,这就算了,它还只给缩略图!?
管他呢,反正没别的图了,直接拿来用吧。
最后加起来,代码是这样的:
def refresh(): global rft,winw,winh,scr_w,scr_h,win while True: try: #print('刷新') #名称 mname,mar=asyncio.run(get_media_name()) nametxt['text']=mname artxt['text']=mar #专辑图(PART1) cover=asyncio.run(get_media_info())['thumbnail'] if rft%50==0:#此处表示每刷新5次才会刷新部分信息,这样可以避免大量读写导致专辑图加载(读取)错误,也可以节省性能 #print('刷新专辑图') #专辑图(PART2) if cover!='NO_DATA': # create the current_media_info dict with the earlier code first thumb_stream_ref = cover # 5MB (5 million byte) buffer - thumbnail unlikely to be larger thumb_read_buffer = Buffer(5000000) # copies data from data stream reference into buffer created above asyncio.run(read_stream_into_buffer(thumb_stream_ref, thumb_read_buffer)) # reads data (as bytes) from buffer buffer_reader = DataReader.from_buffer(thumb_read_buffer) byte_buffer = buffer_reader.read_bytes(thumb_read_buffer.length) with open('media_thumb.jpg', 'wb+') as fobj: fobj.write(bytearray(byte_buffer)) imgf=Image.open("media_thumb.jpg") imgf = imgf.resize((250, 250)) img=ImageTk.PhotoImage(imgf) imgt['image']=img else: imgf=Image.open("media_thumb_none.jpg") imgf = imgf.resize((250, 250)) img=ImageTk.PhotoImage(imgf) imgt['image']=img #亮暗模式 if get_dark(): win.configure(background='#101010') nametxt['bg']='#101010' nametxt['fg']='#FFFFFF' artxt['bg']='#101010' #artxt['fg']='#FFFFFF' imgt['bg']='#101010' prembtn['bg']='#101010' prembtn['fg']='#FFFFFF' nextmbtn['bg']='#101010' nextmbtn['fg']='#FFFFFF' stopbtn['bg']='#0078D7' stopbtn['fg']='#FFFFFF' else: win.configure(background='#FFFFFF') nametxt['bg']='#FFFFFF' nametxt['fg']='#000000' artxt['bg']='#FFFFFF' #artxt['fg']='#000000' imgt['bg']='#FFFFFF' prembtn['bg']='#FFFFFF' prembtn['fg']='#000000' nextmbtn['bg']='#FFFFFF' nextmbtn['fg']='#000000' stopbtn['bg']='#A6D8FF' stopbtn['fg']='#000000' #强制大小 win.geometry(str(winw)+'x'+str(winh)) #刷新间隔,避免循环过于紧凑导致无法控制时间以及产生未知的BUG time.sleep(0.01) rft+=1 except Exception as e: print(e) #raise Exception('信息刷新错误:'+str(e))
界面相关函数
也就三个,主要就是负责一些界面相关的东西
def get_dark(): registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) reg_keypath = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize' try: reg_key = winreg.OpenKey(registry, reg_keypath) except FileNotFoundError: return False for i in range(1024): try: value_name, value, _ = winreg.EnumValue(reg_key, i) if value_name == 'AppsUseLightTheme': return value == 0 except OSError: break return False def close(icon,item): exit() def show_window(icon,item): win.geometry(str(winw)+'x'+str(winh)+'+'+str(int(scr_w-winw-10))+'+'+str(int(scr_h-winh-75))) win.deiconify() def change_icon(icona,item): global use_color_icon,icon,iconimg,menu if not use_color_icon: use_color_icon=True iconimg = Image.open("icon_colorful.png") else: use_color_icon=False iconimg = Image.open("icon.png") icon.stop() icon = pystray.Icon("PyMusicCtrl",iconimg,"音乐控制",menu) icon.run_detached()
getdark,用来获取系统当前的亮暗模式设置。
close,用来在托盘右键,然后选择“退出”后退出软件。
show_window,用来在托盘中选择相应项后显示窗口
change_icon,用来切换彩色/黑白托盘图标
完整代码和图片资源
全打包丢蓝奏云了,自己下载去
参考资料
标题 | 网站 | 链接 |
How can I get the title of the currently playing media in windows 10 with python |
Stack Overflow | https://stackoverflow.com/questions/65011660/how-can-i-get-the-title-of-the-currently-playing-media-in-windows-10-with-python/66037406 |
Detect OS dark mode in Python |
Stack Overflow | https://stackoverflow.com/questions/65294987/detect-os-dark-mode-in-python |
GlobalSystemMediaTransportControlsSession 类 |
Microsoft Docs | https://docs.microsoft.com/zh-cn/uwp/api/windows.media.control.globalsystemmediatransportcontrolssession?view=winrt-22621 |
Creating a system tray icon |
pystray Package Documentation |
https://pystray.readthedocs.io/en/latest/ |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!