之前在用python(PySide2/PyQt5)开发windows触屏应用时遇到一个显而易见的需求:调用触控键盘输入文字。
想实现的效果:
众所周知windows系统(win10和win11)是支持触屏的,系统本身自带两个触控键盘:
其中osk.exe是旧版,功能和界面都比较落后,如今已不再被使用。而TabTip.exe则是触屏windows系统默认使用的触控键盘,点击某些输入框时该键盘就会自动弹出。
如果找到位于C:\Program Files\Common Files\microsoft shared\ink下的TabTip.exe并直接双击运行它,键盘就会自动被呼出。相应地,在python中可直接用一行代码实现:
os.system("C:\\PROGRA~1\\COMMON~1\\MICROS~1\\ink\\tabtip.exe")
看上去好像很简单,然而如果再次双击TabTip.exe就会发现键盘并没有关闭。同时如果先点击键盘右上角的小叉,将键盘关闭,再双击TabTip.exe,键盘也并不会弹出。此时打开任务管理器,会发现里面多了一个进程:
只有将其结束后再次双击TabTip键盘才会弹出,这并不符合想要灵活打开/关闭的需求。那么究竟才能如何完全掌控TabTip.exe呢?
笔者没有找到任何关于它的官方文档,经过一番搜索后终于在一篇帖子里发现了一位大佬探索出来的可以自由开闭TabTip的机制,即:
为接口ITipInvocation创建一个实例并调用其Toggle(HWND)方法,其中HWND应为当前桌面窗口。
上面的帖子里只给出了C以及C#的实现。在这篇帖子中一位大佬给出了python的实现,方法略有不同,不过效果相同:
import win32gui
from ctypes import HRESULT
from ctypes.wintypes import HWND
from comtypes import IUnknown, GUID, COMMETHOD
import comtypes.client
class ITipInvocation(IUnknown):
_iid_ = GUID("{37c994e7-432b-4834-a2f7-dce1f13b834b}")
_methods_ = [
COMMETHOD([], HRESULT, "Toggle",
( ['in'], HWND, "hwndDesktop" )
)
]
dtwin = win32gui.GetDesktopWindow();
ctsdk = comtypes.client.CreateObject("{4ce576fa-83dc-4F88-951c-9d0782b4e376}", interface=ITipInvocation)
ctsdk.Toggle(dtwin);
comtypes.CoUninitialize()
此段代码可以实现键盘的“勾选”式呼出,即每次运行时若键盘未打开则打开,若已打开则关闭。不过前提是TabTip进程必须已经在运行,否则会出现报错:
OSError: [WinError -2147221164] Class not registered
对此,可以先catch该exception后再运行TabTip.exe,即可保证每次TabTip已在运行。
接下来将该过程绑定到LineEdit输入框上。正常情况下应该有两种做法:
个人感觉这两种方法都比较麻烦,此时python的好处就体现出来了,这里可以直接把LineEdit的mousePressEvent()函数用我们自己的方法给替换掉,因为python中万物皆是对象,函数也不例外!
下面的代码用PySide2做了一个带有LineEdit的小窗口用作展示,将LineEdit的mousePressEvent()函数用替换为了自定义的键盘呼出函数popup_keyboard():
from PySide2.QtCore import *
from PySide2.QtWidgets import *
import sys
import win32gui
from ctypes import HRESULT
from ctypes.wintypes import HWND
from comtypes import IUnknown, GUID, COMMETHOD
import comtypes.client
import os
def popup_keyboard(a):
toggle_tabtip()
a.accept()
class ITipInvocation(IUnknown):
_iid_ = GUID("{37c994e7-432b-4834-a2f7-dce1f13b834b}")
_methods_ = [COMMETHOD([], HRESULT, "Toggle", (['in'], HWND, "hwndDesktop"))]
def toggle_tabtip():
try:
comtypes.CoInitialize()
ctsdk = comtypes.client.CreateObject("{4ce576fa-83dc-4F88-951c-9d0782b4e376}", interface=ITipInvocation)
ctsdk.Toggle(win32gui.GetDesktopWindow())
comtypes.CoUninitialize()
except OSError as e:
os.system("C:\\PROGRA~1\\COMMON~1\\MICROS~1\\ink\\tabtip.exe")
class TabTipDemo(QMainWindow):
def __init__(self):
super(TabTipDemo, self).__init__()
self.setObjectName("TabTipDemo")
self.resize(253, 159)
self.centralwidget = QWidget(self)
self.centralwidget.setObjectName("centralwidget")
self.lineEdit = QLineEdit(self.centralwidget)
self.lineEdit.setObjectName("lineEdit")
self.lineEdit.setGeometry(QRect(50, 60, 151, 31))
self.lineEdit.mousePressEvent = popup_keyboard
self.setCentralWidget(self.centralwidget)
self.show()
app = QApplication(sys.argv)
win = TabTipDemo()
app.exec_()
代码运行效果如开头动图所示。
本文实现了在Pyqt/PySide中点击输入框自动弹出触控键盘的功能。Pyqt/PySide目前多被用于开发一些简单的小软件,因此多数时候不会出现一些很复杂的需求,遇到复杂问题经常会找不到解决方法。同时windows的COM编程也是一门大学问,而官方的文档也没有Python版本的,这对使用python开发中大型桌面应用的开发者来说无疑是一个挑战。