双拼练习辅助工具
Note
Cover from 都市シリーズ(City Series) - 17
Why Write?
什么是双拼?
双拼是一种高效的中文拼音输入法,通过 两次按键 输入一个汉字:
- 第一键 代表 声母(如
sh、ch等复合声母对应单键); - 第二键 代表 韵母(如
ang、iong等对应单键)。
特点:
- 输入快(比全拼按键少);
- 需记忆(键位对应声母/韵母);
- 多种方案(如小鹤双拼、微软双拼等)。
举例(小鹤双拼):
- “双” =
u(声母sh) +l(韵母uang) → 按u和l两键。
适合追求输入效率的用户,但需短期学习适应。
为什么写?
然而,我又没有能显示双拼提示的键盘,也不能放一张不透明的图在屏幕上。
于是,灵机一动的我就用Python写了一个程序,用来提示我该敲哪个键。
我也想通过这篇文章练习双拼,于是乎,敲了很久……
Version 1
显示一个界面,引导用户如何使用双拼——如何击键。
比如 “上” shang 字,就要把它拆成 sh (U)和ang (H)。

Code
# -*- coding: UTF-8 -*-
"""
PROJECT_NAME ShuangPin
PRODUCT_NAME PyCharm
NAME main
AUTHOR Pfolg
TIME 2025/8/10 16:59
"""
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QLabel, QWidget, QApplication, QFrame
key_data = {
# 自然码
"Q": ["", "iu", ""],
"W": ["", "ia", "ua"],
"E": ["", "", ""],
"R": ["", "uan", ""],
"T": ["", "ue", "ve"],
"Y": ["", "ing", "uai"],
"U": ["sh", "", ""],
"I": ["ch", "", ""],
"O": ["", "uo", ""],
"P": ["", "un", ""],
# ---
"A": ["", "", ""],
"S": ["", "iong", "ong"],
"D": ["", "iang", "uang"],
"F": ["", "en", ""],
"G": ["", "eng", ""],
"H": ["", "ang", ""],
"J": ["", "an", ""],
"K": ["", "ao", ""],
"L": ["", "ai", ""],
# ---
"Z": ["", "ei", ""],
"X": ["", "ie", ""],
"C": ["", "iao", ""],
"V": ["zh", "ui", ""],
"B": ["", "ou", ""],
"N": ["", "in", ""],
"M": ["", "ian", ""],
}
class MyKey(QLabel):
def __init__(self, x: int, y: int, main_key: str, func_key: str, k1: str, k2: str, w=80):
super().__init__()
self.setGeometry(x, y, w, w)
self.main_key = QLabel(main_key, self)
self.func_key = QLabel(func_key, self)
self.func_key.setStyleSheet("color:red;")
self.k1 = QLabel(k1, self)
self.k2 = QLabel(k2, self)
font = QFont()
font.setPointSize(16)
self.main_key.setFont(font)
self.setFrameShape(QFrame.Shape.Box)
# 位置
self.main_key.setGeometry(0, 0, int(w / 2), int(w / 2))
self.func_key.setGeometry(int(w / 2), 0, int(w / 2), int(w / 2))
self.k1.setGeometry(0, int(w / 2), int(w / 2), int(w / 2))
self.k2.setGeometry(int(w / 2), int(w / 2), int(w / 2), int(w / 2))
# 样式
self.main_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.func_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k1.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k2.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("color:#89ca78;background-color:rgba(12, 49, 110, 20)")
# 字体
font.setPointSize(12)
self.func_key.setFont(font)
self.k1.setFont(font)
self.k2.setFont(font)
class KWindow(QWidget):
def __init__(self):
super().__init__()
# 10*20*3 |-10 9*20 |-20 7*20 # 20->60px
self.key_width = 80
sw, sh = app.primaryScreen().geometry().width(), app.primaryScreen().geometry().height()
w, h = 10 * self.key_width, 3 * self.key_width
self.setGeometry(int((sw - w) / 2), int(sh - h - self.key_width), w, h)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowTransparentForInput |
Qt.WindowType.ToolTip |
Qt.WindowType.WindowStaysOnTopHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
key_keys = list(key_data.keys())
loc_x, loc_y = 0, 0
for i in range(len(key_keys)):
k = key_keys[i]
a = MyKey(loc_x, loc_y, k, key_data.get(k)[0], key_data.get(k)[1], key_data.get(k)[2])
a.setParent(self)
if i == 9 or i == 18:
loc_y += self.key_width
loc_x = loc_y / 2
else:
loc_x += self.key_width
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setFont("Maple Mono NF CN Medium")
root = KWindow()
root.show()
sys.exit(app.exec())
Version 2
无法使用:由于线程问题,程序在运行时会崩溃。
在击键时显示按下的键。

Code
# -*- coding: UTF-8 -*-
"""
PROJECT_NAME ShuangPin
PRODUCT_NAME PyCharm
NAME main
AUTHOR Pfolg
TIME 2025/8/10 16:59
"""
import os
import socket
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QAction, QIcon
from PySide6.QtWidgets import QLabel, QWidget, QApplication, QFrame, QSystemTrayIcon, QMenu
from pynput import keyboard
"""pyinstaller --onefile --windowed --add-data "sp.png;." main.py"""
def resource_path(relative_path):
"""获取资源的绝对路径(兼容开发环境和打包后环境)"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
logo = resource_path("sp.png")
# 使用示例
ui_path = resource_path("typing.ui")
key_data = {
# 自然码
"Q": ["", "iu", ""],
"W": ["", "ia", "ua"],
"E": ["", "", ""],
"R": ["", "uan", ""],
"T": ["", "ue", "ve"],
"Y": ["", "ing", "uai"],
"U": ["sh", "", ""],
"I": ["ch", "", ""],
"O": ["", "uo", ""],
"P": ["", "un", ""],
# ---
"A": ["", "", ""],
"S": ["", "iong", "ong"],
"D": ["", "iang", "uang"],
"F": ["", "en", ""],
"G": ["", "eng", ""],
"H": ["", "ang", ""],
"J": ["", "an", ""],
"K": ["", "ao", ""],
"L": ["", "ai", ""],
# ---
"Z": ["", "ei", ""],
"X": ["", "ie", ""],
"C": ["", "iao", ""],
"V": ["zh", "ui", ""],
"B": ["", "ou", ""],
"N": ["", "in", ""],
"M": ["", "ian", ""],
}
class MyKey(QLabel):
def __init__(self, x: int, y: int, main_key: str, func_key: str, k1: str, k2: str, w=80):
super().__init__()
self.setGeometry(x, y, w, w)
self.main_key = QLabel(main_key, self)
self.func_key = QLabel(func_key, self)
self.func_key.setStyleSheet("color:red;")
self.k1 = QLabel(k1, self)
self.k2 = QLabel(k2, self)
font = QFont()
font.setPointSize(16)
self.main_key.setFont(font)
self.setFrameShape(QFrame.Shape.Box)
# 位置
self.main_key.setGeometry(0, 0, int(w / 2), int(w / 2))
self.func_key.setGeometry(int(w / 2), 0, int(w / 2), int(w / 2))
self.k1.setGeometry(0, int(w / 2), int(w / 2), int(w / 2))
self.k2.setGeometry(int(w / 2), int(w / 2), int(w / 2), int(w / 2))
# 样式
self.main_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.func_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k1.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k2.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(12, 49, 110, 20)"
)
# 字体
font.setPointSize(12)
self.func_key.setFont(font)
self.k1.setFont(font)
self.k2.setFont(font)
class KWindow(QWidget):
def __init__(self):
super().__init__()
# 10*20*3 |-10 9*20 |-20 7*20 # 20->60px
self.key_width = 80
sw, sh = app.primaryScreen().geometry().width(), app.primaryScreen().geometry().height()
w, h = 10 * self.key_width, 3 * self.key_width
self.setGeometry(int((sw - w) / 2), int(sh - h - self.key_width), w, h)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowTransparentForInput |
Qt.WindowType.ToolTip |
Qt.WindowType.WindowStaysOnTopHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
key_keys = list(key_data.keys())
loc_x, loc_y = 0, 0
for i in range(len(key_keys)):
k = key_keys[i]
a = MyKey(loc_x, loc_y, k, key_data.get(k)[0], key_data.get(k)[1], key_data.get(k)[2])
a.setObjectName(k)
a.setParent(self)
if i == 9 or i == 18:
loc_y += self.key_width
loc_x = loc_y / 2
else:
loc_x += self.key_width
self.keyboardListener = keyboard.Listener(on_press=self.on_key_press, on_release=self.on_key_release)
self.keyboardListener.start()
@staticmethod
def on_key_press(key):
"""处理键盘按下事件"""
try:
# 处理普通按键
key_name = key.char.upper()
print(key_name)
k: MyKey = root.findChild(MyKey, key_name)
k.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(255, 208, 75, 80);"
)
except Exception as e:
print(str(e))
return
@staticmethod
def on_key_release(key):
"""处理键盘释放事件"""
try:
# 处理普通按键
key_name = key.char.upper()
print(key_name)
k: MyKey = root.findChild(MyKey, key_name)
k.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(12, 49, 110, 20)"
)
except Exception as e:
print(str(e))
return
def single_instance(port: int):
try:
# 选择一个不常用的端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", port))
except socket.error:
print("另一个实例正在运行,退出。")
sys.exit(1)
return sock
class Tray(QSystemTrayIcon):
def __init__(self):
super().__init__()
self.menu = QMenu()
ac_q = QAction("Quit", self.menu)
ac_q.triggered.connect(sys.exit)
self.menu.addAction(ac_q)
self.setContextMenu(self.menu)
self.setIcon(QIcon(logo))
self.setToolTip("双拼练习辅助工具")
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setFont("Maple Mono NF CN Medium")
root = KWindow()
root.show()
t = Tray()
ins = single_instance(7086)
sys.exit(app.exec())
Version 3
在基于1、2版本的基础上,沿用了界面,使用AI优化了线程。

Code
# -*- coding: UTF-8 -*-
import os
import socket
import sys
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QFont, QAction, QIcon
from PySide6.QtWidgets import QLabel, QWidget, QApplication, QFrame, QSystemTrayIcon, QMenu
from pynput import keyboard
def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
logo = resource_path("sp.png")
key_data = {
# 自然码
"Q": ["", "iu", ""],
"W": ["", "ia", "ua"],
"E": ["", "", ""],
"R": ["", "uan", ""],
"T": ["", "ue", "ve"],
"Y": ["", "ing", "uai"],
"U": ["sh", "", ""],
"I": ["ch", "", ""],
"O": ["", "uo", ""],
"P": ["", "un", ""],
# ---
"A": ["", "", ""],
"S": ["", "iong", "ong"],
"D": ["", "iang", "uang"],
"F": ["", "en", ""],
"G": ["", "eng", ""],
"H": ["", "ang", ""],
"J": ["", "an", ""],
"K": ["", "ao", ""],
"L": ["", "ai", ""],
# ---
"Z": ["", "ei", ""],
"X": ["", "ie", ""],
"C": ["", "iao", ""],
"V": ["zh", "ui", ""],
"B": ["", "ou", ""],
"N": ["", "in", ""],
"M": ["", "ian", ""],
}
class KeyboardListener(QThread):
key_pressed = Signal(str)
key_released = Signal(str)
def run(self):
with keyboard.Listener(
on_press=self.on_press,
on_release=self.on_release
) as listener:
listener.join()
def on_press(self, key):
try:
if hasattr(key, 'char') and key.char:
self.key_pressed.emit(key.char.upper())
except Exception as e:
print(f"Press error: {e}")
def on_release(self, key):
try:
if hasattr(key, 'char') and key.char:
self.key_released.emit(key.char.upper())
except Exception as e:
print(f"Release error: {e}")
class MyKey(QLabel):
def __init__(self, x: int, y: int, main_key: str, func_key: str, k1: str, k2: str, w=80):
super().__init__()
self.setGeometry(x, y, w, w)
self.main_key = QLabel(main_key, self)
self.func_key = QLabel(func_key, self)
self.func_key.setStyleSheet("color:red;")
self.k1 = QLabel(k1, self)
self.k2 = QLabel(k2, self)
font = QFont()
font.setPointSize(16)
self.main_key.setFont(font)
self.setFrameShape(QFrame.Shape.Box)
self.main_key.setGeometry(0, 0, int(w / 2), int(w / 2))
self.func_key.setGeometry(int(w / 2), 0, int(w / 2), int(w / 2))
self.k1.setGeometry(0, int(w / 2), int(w / 2), int(w / 2))
self.k2.setGeometry(int(w / 2), int(w / 2), int(w / 2), int(w / 2))
self.main_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.func_key.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k1.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.k2.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(171, 111, 200, 50)"
)
font.setPointSize(12)
self.func_key.setFont(font)
self.k1.setFont(font)
self.k2.setFont(font)
class KWindow(QWidget):
def __init__(self):
super().__init__()
self.key_width = 80
screen = QApplication.primaryScreen().geometry()
w, h = 10 * self.key_width, 3 * self.key_width
self.setGeometry(
int((screen.width() - w) / 2),
int(screen.height() - h - self.key_width),
w, h
)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowTransparentForInput |
Qt.WindowType.ToolTip |
Qt.WindowType.WindowStaysOnTopHint
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
key_keys = list(key_data.keys())
loc_x, loc_y = 0, 0
for i, k in enumerate(key_keys):
a = MyKey(loc_x, loc_y, k, key_data[k][0], key_data[k][1], key_data[k][2], self.key_width)
a.setParent(self)
a.setObjectName(k)
if i in [9, 18]: # 行尾换行
loc_y += self.key_width
loc_x = loc_y / 2
else:
loc_x += self.key_width
# 创建并启动键盘监听线程
self.listener_thread = KeyboardListener()
self.listener_thread.key_pressed.connect(self.handle_key_press)
self.listener_thread.key_released.connect(self.handle_key_release)
self.listener_thread.start()
def handle_key_press(self, key_name):
"""处理按键按下事件(UI线程安全)"""
key_widget = self.findChild(MyKey, key_name)
if key_widget:
key_widget.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(255, 208, 75, 80);"
)
def handle_key_release(self, key_name):
"""处理按键释放事件(UI线程安全)"""
key_widget = self.findChild(MyKey, key_name)
if key_widget:
key_widget.setStyleSheet(
"color:#ffffff;"
"background-color:rgba(171, 111, 200, 50)"
)
class Tray(QSystemTrayIcon):
def __init__(self):
super().__init__()
self.menu = QMenu()
ac_q = QAction("退出", self.menu)
ac_q.triggered.connect(sys.exit)
self.menu.addAction(ac_q)
self.setContextMenu(self.menu)
self.setIcon(QIcon(logo))
self.setToolTip("双拼练习辅助工具")
self.show()
def single_instance(port: int):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", port))
return sock
except socket.error:
print("另一个实例正在运行,退出。")
sys.exit(1)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setFont("Maple Mono NF CN Medium")
# 确保单实例运行
instance_lock = single_instance(7086)
window = KWindow()
window.show()
tray = Tray()
sys.exit(app.exec())
Note
Cover Information
ASJC 最も人気のあるの日本語曲《I Miss You》がリリースされ、
以下のプラットフォームで聴くことができます。
Full MV : https://youtu.be/Eur8P7Nku9A
KKBOX : https://kkbox.fm/VaIRl5
YT Music : <https://music.youtube.com/playlist?list=OLAK5uy_nRhhgO2VyOwqw6bxWBFkg9QhaguIqrQsI
Spotify : https://open.spotify.com/album/4HPurCZhsb6suc2aRUZPMa>
Apple Music : https://music.apple.com/album/1799128465
Amazon : https://music.amazon.co.uk/albums/B0DDRND9ZW?marketplaceId=A1F83G8C2ARO7P&musicTerritory=GB&ref=dm_sh_epFPtLDjZPO0uoG0NW5VPFXuh
「ASJC is available now on Spotify、Amazon Music、Apple Music、Youtube Music、KKBOX 」