查拼音程序升级版

之前做过一个查拼音程序,只能查一个字,并且无法复制查到的拼音,今天对它升个级,可以查任意多字符,并且可复制拼音,运行效果如图:

程序代码:

python 复制代码
# show_pinyin.py
import sys
import os
import keyboard
import pyperclip
from pypinyin import pinyin, Style
import tkinter as tk
import threading
import time
import re
import ctypes
from queue import Queue
import pystray
from PIL import Image, ImageDraw
import signal
 
 
class PinyinDisplay:
    def __init__(self):
        # 主Tk根窗口(隐藏),确保在主线程创建并运行
        self.tk_root = tk.Tk()
        self.tk_root.withdraw()
 
        # 弹出窗口引用(Toplevel),在主线程中创建/销毁
        self.popup_win = None
        self.popup_label = None
        self.window_open = False
 
        # 请求队列:后台线程放入显示请求,主线程(tk)处理
        self.request_queue = Queue()
 
        self.is_running = True
        self.current_pinyin = ""  # 存储当前显示的拼音
        self.tray_icon = None
        self.tray_thread = None
        # 用于在主循环中等待退出信号
        self.stop_event = threading.Event()
 
    def create_tray_icon(self):
        """创建系统托盘图标"""
        # 创建一个简单的图标
        image = self.create_icon_image()
 
        # 创建托盘图标
        self.tray_icon = pystray.Icon(
            "pinyin_display",
            image,
            "拼音显示工具",
            menu=pystray.Menu(
                pystray.MenuItem("显示拼音 (Ctrl+')", lambda icon, item: self.show_pinyin()),
                pystray.MenuItem("退出程序", lambda icon, item: self.quit_program())
            )
        )
 
        # 设置鼠标悬停提示
        self.tray_icon.title = self.get_tooltip_text()
 
        # 在新线程中运行托盘图标
        # 使用非 daemon 线程,这样可以在退出时 join 等待其正确清理图标
        self.tray_thread = threading.Thread(target=self.tray_icon.run, daemon=False)
        self.tray_thread.start()
 
    def create_icon_image(self):
        """创建托盘图标图像"""
        # 创建一个简单的图标 - 一个带有"拼"字的图标
        size = 64
        image = Image.new('RGBA', (size, size), (0, 0, 0, 0))
        draw = ImageDraw.Draw(image)
 
        # 绘制圆形背景
        draw.ellipse([4, 4, size - 4, size - 4], fill=(0, 255, 0, 255))
 
        # 绘制"拼"字
        from PIL import ImageFont
        try:
            # 尝试使用系统中文字体
            font = ImageFont.truetype("msyh.ttf", 40)  # Microsoft YaHei
        except:
            try:
                font = ImageFont.truetype("simhei.ttf", 40)  # SimHei
            except:
                try:
                    font = ImageFont.truetype("arial.ttf", 40)
                except:
                    font = ImageFont.load_default()
 
        # 计算文字位置
        text = "拼"
        bbox = draw.textbbox((0, 0), text, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
        x = (size - text_width) // 2
        y = (size - text_height) // 2 - 2
 
        draw.text((x, y), text, fill=(255, 0, 0, 255), font=font)
 
        return image
 
    def get_tooltip_text(self):
        """获取托盘图标提示文本"""
        tooltip_lines = [
            "拼音显示工具",
            "━━━━━━━━━━━━━━━━━━━",
            "按下 Ctrl+' 显示选中中文的拼音",
            "右键点击拼音窗口 -> 复制拼音",
            "左键点击拼音窗口 -> 关闭窗口"
        ]
        return "\n".join(tooltip_lines)
 
 
    def quit_program(self):
        """退出程序"""
        print("\n正在退出程序...")
        self.is_running = False
        # 发送停止信号给监听循环
        try:
            self.stop_event.set()
        except Exception:
            pass
        self.cleanup()
        # 强制退出(保底)
        try:
            os._exit(0)
        except Exception:
            pass
 
    def get_selected_text(self):
        """获取当前选中的文本"""
        try:
            # 保存当前剪贴板内容
            original_clipboard = pyperclip.paste()
 
            # 模拟Ctrl+C复制选中的文本
            keyboard.press_and_release('ctrl+c')
 
            # 等待剪贴板变化(最多等待0.5秒),防止长时间阻塞
            end = time.time() + 0.5
            selected_text = None
            while time.time() < end:
                try:
                    cur = pyperclip.paste()
                except Exception:
                    cur = None
                if cur is not None and cur != original_clipboard:
                    selected_text = cur
                    break
                time.sleep(0.05)
 
            # 恢复原始剪贴板内容(尽量恢复)
            try:
                pyperclip.copy(original_clipboard)
            except Exception:
                pass
 
            # 如果没有检测到剪贴板变化,认为没有新的选中内容
            if not selected_text:
                return None
 
            return selected_text.strip()
        except Exception as e:
            print(f"获取选中文本时出错: {e}")
            return None
 
    def is_chinese_text(self, text):
        """检查文本是否包含中文"""
        if not text:
            return False
        # 检查是否包含中文字符
        # chinese_pattern = re.compile(r'[\u4e00-\u9fff]')
        # 一个较为完整的Unicode汉字区块匹配范围
        chinese_pattern = re.compile(r'[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u20000-\u2A6DF]')
        return bool(chinese_pattern.search(text))
 
    def text_to_pinyin(self, text):
        """将中文文本转换为拼音(带声调)"""
        if not text:
            return ""
 
        # 使用pypinyin转换,保留声调
        result = pinyin(text, style=Style.TONE, heteronym=False)
        # 将结果转换为字符串
        pinyin_list = [item[0] for item in result if item]
        return ' '.join(pinyin_list)
 
    def process_queue(self):
        """主线程(tk)定期调用,处理显示请求队列"""
        try:
            while not self.request_queue.empty():
                selected_text, pinyin_text, cursor_x, cursor_y = self.request_queue.get_nowait()
                # 若已有窗口打开,先关闭它
                if self.window_open:
                    self.close_window()
                # 创建Toplevel窗口在主线程
                self.popup_win = tk.Toplevel(self.tk_root)
                self.popup_win.overrideredirect(True)
                self.popup_win.attributes('-topmost', True)
                self.popup_win.attributes('-alpha', 0.95)
 
                frame = tk.Frame(self.popup_win, bg="#FFFFE0", bd=1, relief="solid")
                frame.pack(fill="both", expand=True)
 
                self.popup_label = tk.Label(
                    frame,
                    text=pinyin_text,
                    font=("Microsoft YaHei", 14, "bold"),
                    bg="#FFFFE0",
                    fg="#333333",
                    padx=20,
                    pady=12,
                    wraplength=600,
                    justify="left",
                    cursor="hand2"
                )
                self.popup_label.pack(fill="both", expand=True)
 
                hint = tk.Label(frame, text="右键点击窗体复制拼音 | 左键点击窗体关闭", font=("Microsoft YaHei", 9), bg="#FFF8DC", fg="#666666")
                hint.pack(fill="x")
 
                # 绑定事件
                self.popup_win.bind('<Button-1>', lambda e: self.close_window())
                self.popup_win.bind('<Button-3>', lambda e: (pyperclip.copy(pinyin_text), self.show_message("✓ 已复制到剪贴板")))
 
                # 位置计算
                self.popup_win.update_idletasks()
                w = self.popup_win.winfo_width()
                h = self.popup_win.winfo_height()
                screen_w = self.popup_win.winfo_screenwidth()
                screen_h = self.popup_win.winfo_screenheight()
 
                x_pos = int(cursor_x + 20)
                y_pos = int(cursor_y + 20)
                if x_pos + w > screen_w:
                    x_pos = int(cursor_x - w - 20)
                if y_pos + h > screen_h:
                    y_pos = int(cursor_y - h - 20)
                if x_pos < 0:
                    x_pos = 10
                if y_pos < 0:
                    y_pos = 10
 
                self.popup_win.geometry(f"+{x_pos}+{y_pos}")
                self.window_open = True
                self.current_pinyin = pinyin_text
        except Exception:
            pass
        finally:
            # 继续轮询
            try:
                self.tk_root.after(100, self.process_queue)
            except Exception:
                pass
 
    def get_cursor_position(self):
        """获取鼠标当前位置(用于定位窗口),使用 Win32 API 获得全局坐标"""
        try:
            class POINT(ctypes.Structure):
                _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
            pt = POINT()
            ctypes.windll.user32.GetCursorPos(ctypes.byref(pt))
            return int(pt.x), int(pt.y)
        except Exception:
            # 退回到 tkinter 获取(如果可用)或默认值
            try:
                return self.tk_root.winfo_pointerx(), self.tk_root.winfo_pointery()
            except Exception:
                return 100, 100
 
    def show_message(self, message):
        """在弹窗上显示临时消息(线程安全):将 UI 更新通过 tk_root.after 安排到主线程执行"""
        try:
            if self.popup_label and self.window_open:
                original_text = self.popup_label.cget('text')
                original_fg = self.popup_label.cget('fg')
                self.popup_label.config(text=message, fg="#0066CC")
                # 1秒后恢复显示拼音
                self.tk_root.after(1000, lambda: self.popup_label.config(text=original_text, fg=original_fg))
        except Exception:
            pass
 
    def close_window(self):
        """关闭拼音显示窗口(在主线程调用)"""
        try:
            if self.popup_win and self.window_open:
                try:
                    self.popup_win.destroy()
                except Exception:
                    pass
                self.popup_win = None
                self.popup_label = None
                self.window_open = False
                self.current_pinyin = ""
        except Exception:
            pass
 
    def check_outside_click(self):
        """轮询检测:当拼音窗口打开时,若鼠标左键在窗口外被按下则关闭窗口。
        使用 Win32 API(GetAsyncKeyState + GetCursorPos)保证在 overrideredirect 窗口下也可靠。"""
        try:
            if self.window_open and self.popup_win:
                # VK_LBUTTON = 0x01;最高位为 1 表示按键当前处于按下状态
                state = ctypes.windll.user32.GetAsyncKeyState(0x01)
                if state & 0x8000:
                    cursor_x, cursor_y = self.get_cursor_position()
                    try:
                        win_x = self.popup_win.winfo_x()
                        win_y = self.popup_win.winfo_y()
                        win_w = self.popup_win.winfo_width()
                        win_h = self.popup_win.winfo_height()
                    except Exception:
                        win_x = win_y = win_w = win_h = 0
                    inside = (win_x <= cursor_x <= win_x + win_w and
                              win_y <= cursor_y <= win_y + win_h)
                    if not inside:
                        self.close_window()
        except Exception:
            pass
        finally:
            try:
                self.tk_root.after(50, self.check_outside_click)
            except Exception:
                pass

    def show_pinyin(self):
        """热键或托盘菜单触发的显示入口:在后台线程执行,采集文本并把显示请求放入队列,由主线程(tk)处理弹窗。"""
        # 如果拼音窗口已经打开,直接返回
        if self.window_open:
            # print("拼音已显示在拼音窗口")
            return
 
        selected_text = self.get_selected_text()
        if not selected_text:
            # print("未选中任何文本")
            return
        if not self.is_chinese_text(selected_text):
            # print("未检测到中文文本")
            return
 
        pinyin_text = self.text_to_pinyin(selected_text)
        if not pinyin_text:
            return
 
        # print(f"原文: {selected_text}")
        # print(f"拼音: {pinyin_text}")
 
        # 获取鼠标位置(在后台线程用Win32 API)
        try:
            cursor_x, cursor_y = self.get_cursor_position()
        except Exception:
            cursor_x, cursor_y = 100, 100
 
        # 将请求放到队列,主线程会在 process_queue 中处理
        try:
            self.request_queue.put((selected_text, pinyin_text, cursor_x, cursor_y))
        except Exception as e:
            print(f"请求入队失败: {e}")
 
 
    def start_listener(self):
        """启动热键监听"""
        print("=" * 50)
        print("拼音显示程序已在系统托盘运行!")
        print("鼠标悬停托盘图标查看使用说明")
        print("=" * 50)
 
        # 创建系统托盘图标
        self.create_tray_icon()
 
        # 注册热键(回调在后台线程执行)
        try:
            keyboard.add_hotkey("ctrl+'", self.show_pinyin)
            print("✓ 热键 Ctrl+' 已注册")
        except Exception as e:
            print(f"✗ 注册热键失败: {e}")
            print("请确保以管理员权限运行程序")
            return
 
        # 启动主线程的队列轮询(Tk主循环中)
        try:
            self.tk_root.after(100, self.process_queue)
        except Exception:
            pass
        
        # 启动窗口外点击检测轮询(用于点击窗口外关闭拼音窗口)
        try:
            self.tk_root.after(50, self.check_outside_click)
        except Exception:
            pass

        # 运行Tk主循环(阻塞,确保所有Tk窗口在主线程创建/销毁)
        try:
            self.tk_root.deiconify()  # 确保tk主循环就绪(窗口仍保持隐藏)
            self.tk_root.withdraw()
            self.tk_root.mainloop()
        except KeyboardInterrupt:
            pass
        finally:
            print("\n程序正在退出...")
            self.cleanup()
 
    def cleanup(self):
        """清理资源"""
        self.is_running = False
        try:
            self.close_window()
        except Exception:
            pass
 
        # 移除所有热键
        try:
            keyboard.unhook_all_hotkeys()
        except Exception:
            pass
 
        # 关闭托盘图标并等待线程退出,避免图标残留
        if self.tray_icon:
            try:
                # 尝试先设置不可见
                try:
                    self.tray_icon.visible = False
                except Exception:
                    pass
                # 请求停止托盘图标循环
                try:
                    self.tray_icon.stop()
                except Exception:
                    pass
            finally:
                # 等待托盘线程结束
                try:
                    if self.tray_thread and self.tray_thread.is_alive():
                        self.tray_thread.join(timeout=2)
                except Exception:
                    pass
                self.tray_icon = None
                self.tray_thread = None
 
        # 退出Tk主循环(如果在运行)
        try:
            if self.tk_root:
                self.tk_root.quit()
                self.tk_root.destroy()
        except Exception:
            pass
 
        print("程序已退出")
 
def signal_handler(signum, frame):
    """信号处理函数"""
    print("\n收到退出信号,正在退出...")
    sys.exit(0)
 
 
def main():
    # 注册信号处理
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
 
    # 创建实例并启动
    app = PinyinDisplay()
    try:
        app.start_listener()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(f"程序运行出错: {e}")
    finally:
        app.cleanup()
 
 
if __name__ == "__main__":
    main()

程序运行前提:

pip install keyboard pyperclip pypinyin pillow pystray

cx_Freeze打包为exe脚本:

python 复制代码
from cx_Freeze import setup, Executable
 
setup(
    name="search_pinyin",
    version="1.0",
    description="查拼音",
    options={
        "build_exe": {
            "packages": ["tkinter",
                         "pyperclip",
                         "pypinyin",
                         "keyboard",
                         "pystray",
                         "PIL",
                         'ctypes',
                         "os",
                         "sys",
                         "threading",
                         "time"],  # 程序所使用的依赖包
            "excludes": ["unittest", "email", "html", "http", "xmlrpc", "pydoc"],  # 排除不必要的包
            "optimize": 2  # 优化级别
        }
    },
    executables=[
        Executable(
            "show_pinyin.py",
            target_name="show_pinyin.exe",
            base="gui"  # 使用GUI以避免显示控制台窗口
        )
    ]
)
相关推荐
旧梦952715 分钟前
Java 枚举类详解:从基础语法到高级用法
java·开发语言·python
小灰灰搞电子19 分钟前
分享自己写的一个通信协议源码,支持C++和C,双包头+不定长
c语言·开发语言·c++
2601_9621741724 分钟前
Spring 核心技术解析【纯干货版】- XI:Spring 数据访问模块 Spring-Oxm 模块精讲
数据库·python·spring
Brilliantwxx32 分钟前
【C++】初入嵌入式C++复习-----经典面试题100道
开发语言·c++·算法·面试·职场和发展
海兰33 分钟前
【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(二)
javascript·python·金融
Freak嵌入式42 分钟前
告别死循环!Pico 外部中断控制 LED,比轮询高效 100 倍
java·开发语言·stm32·单片机·嵌入式硬件
nangonghen42 分钟前
Agent如何传递不同类型的ID给MCP Server
开发语言·python
weixin_398831661 小时前
佛山AI推广GEO哪家做的专业
人工智能·python
luj_17681 小时前
断舍离中的项目管理智慧
c语言·开发语言·c++·经验分享·算法