Skróty klawiszowe w Windows - narzędzie w Pythonie
Korzystałaś kiedyś z StumpWM? Jeśli tak, to wiesz, jak wygodna może być praca z systemem okienkowym bez użycia myszy. Jeśli masz dużo do zrobienia, to każda chwila się liczy i szkoda czasu na walkę z myszą, szukanie ,bookmarków', skrótów do aplikacji itd. Ponieważ nie znalazłem narzędzia, które by mnie satysfakcjonowało, napisałem sobie narzędzie do bookmarków, uruchamiania aplikacji itd przy pomocy ,,skrótów'' klawiszowych. Poniżej prezentuję jak działa i zamieszczam kod źródłowy, żebyś też mógł sobie usprawnić pracę. Przy okazji możesz potraktować to jako tutorial (wprowadzenie?) do języka Python, PyQt, klas w Pythonie, obsługi wyjątków, uruchamiania innych procesów i poleceń systemowych, regexpów itd:) Z tego powodu opiszę też krok po kroku, jak program działa.
Table of Contents
1 (Po) co to robi?
Być może mam słabą pamięć, a może nie lubię zaśmiecać umysłu, ale zaskakująco dużo czasu zajmowało mi szukanie, gdzie jest skrót do jakieś aplikacji, dokumentu, jaki był adres strony z xyz, walka z interfejsem Windows by uruchomić cmd jako administrator1 i tym podobne działania. Niestety, Windows nie umożliwia modyfikowania skrótów klawiszowych na przyzwoitym poziomie, więc napisałem sobie więc aplikacyjkę (skrypt?). Do aplikacji mam skrót na pulpicie, z przypisanym skrótem klawiszowym (Ctrl+Alt+]). Aplikacja ta po uruchomieniu:
- wyświetla malutkie okienko z polem do wprowadzania,
- odczytuje wprowadzoną zawartość,
- jeżeli zawartość pasuje do zdefiniowanego wzorca, uruchamia procedurę w pythonie i się zamyka,
- jeżeli zawartość nie pasuje, to pozwala wprowadzać kolejny znak.
Pytonowe procedury, wywoływane przez programik uruchamiają zewnętrzne aplikacje (procedury run, runie) lub nie (procedury help, cc).
2 Jak działa?
Programik wykorzystuje oprócz biblioteki standardowej PyQT, więc musisz oprócz Pythona3 mieć także to. UI przygotowane jest w designerze, więc tej części kodu nie będę omawiał.
2.1 Część inicjująca
Część ta znajduje się na końcu:) Jak to w Pythonie - poznasz ją po konstrukcji
if __name__ == "__main__":
To, co tam się znajduje inicjuje okienko Qt i ustawia jego flagi - ukrywa pasek tytułu (CustomizeWindowHint) i ustawia, żeby było zawsze na wierzchu (WindowStayOnTopHint). Wreszcie - inicjuje klasę HotkeyL, która dziedziczy z UiMainWindow - klasy zdefiniowanej automatycznie przez kod generowany z Designera.
Klasa ta oprócz przeładowanej metody setupUi ma także metodę setupUi2, w której podłączone są:
- skróty klawiszowe aplikacji - Ctrl+w i ESC zamykające aplikację,
- metoda getHotkey, wywoływana na skutek sygnału textChanged, czyli po wpisaniu czegokolwiek do okienka - to ona wykonuje (prawie) całą robotę.
Po uruchomieniu setupUi2 kod wrzuca do statusBara (wiersza na dole okienka) informację o poprawnym uruchomieniu i uruchamia pętlę główną programu.
A co konkretnie robi metoda getHotkey?
2.2 getHotkey
def getHotkey(self,str): if (str=='e'):self.run(r"C:/tools/emacs/emacs-24.3/bin/emacs.exe") #run emacs [...] if (str=='c'):self.statusBar.showMessage("run commandline...")#run commandline if (str=='cc'):self.run("cmd")#cmd if (str=='cp'):self.run("powershell")#powershell [...] if (str=='r'):self.statusBar.showMessage("run program...")#run program if (str=='ro'):self.run(r"C:Program Files (x86)Microsoft OfficeOffice14Outlook.exe")#run outlook
Metoda podłączona do sygnału textChanged otrzymuje przy każdej zmianie QString (string) zawierający tekst, wpisany do pola edycji. W kolejnych instrukcjach if sprawdza wprowadzony tekst. Jeśli np jest równy cc, to wywołuje funkcję:
self.run("cmd")
która uruchamia ,,cmd'' i kończy działanie programu hotkey. Zauważ, że już gdy wpiszesz ,,c'', w statusBarze pojawi się komunikat run commandline…, ale program nie przerwie działania i pozwoli dopisać np. c.
Definiując kolejne if-y możemy dowolnie dodać sobie (wieloklawiszowe) skróty do czegokolwiek, co potrafimy oskryptować w Pythonie i bibliotekach pomocniczych.
2.3 run
Metoda run wygląda tak:
def run(self,*str,cwd=None): from subprocess import Popen Popen( *str ,cwd=cwd) sys.exit() return
Metoda ta przyjmuje parametr(y) str i cwd, importuje bibliotekę Popen, służącą do obsługi podprocesów i wywołuje parametr(y) str w opcjonalnej ścieżce cwd. Co to znaczy? Popen przyjmuje (prawie) dowolnie dużo parametrów - jesli np. chcesz uruchomić polecenie powershell -c gci -recurse w ścieżce h:, to możesz to zrobić przez:
Popen('powershell','-c','gci','-recurse',cwd=r'h:')
a Popen złoży to w jedno polecenie. Parametr przekazany z * (czyli w tym przypadku *str) zostanie ,,rozpakowany'' i przekazany do Popen. Nazwany parametr cwd wskaże ścieżkę, w której polecenie ma zostać uruchomione.
Na koniec funkcja wykonuje sys.exit(), żeby zakończyć działanie hotkeya (bo już nam nie jest potrzebny - mamy uruchomione wybrane polecenie).
Import Popen-a wykonywany jest celowo dopiero tutaj - w części przypadków nie jest potrzebny, więc szkoda czekać na ten import.
2.4 runie - jak otworzyć url w już otwartym oknie Internet Explorera?
Metoda runie jest ciekawa, bo wykorzystuje windowsowe COM-api do uruchomienia Internet Explorera. Łatwo można ją przerobić do innych celów.
Internet Explorer jest ciekawą przeglądarką - wśród features posiada taki, że nie da się z wiersza poleceń otworzyć ścieżki w już istniejącym oknie. Można to objeść, stosując własnie COM API. self.ie=win32com.client.Dispatch("InternetExplorer.Application") uruchamia IE, natomiast self.ie.Navigate2(url,navOpenInBackgroundTab) otwiera url zawarty w zmiennej url jako nową zakładkę w otwartym dispatchem oknie.
Metoda nie jest jeszcze doskonała - odwołuje się do okna otwartego dispatchem, więc kolejne urle można otwierać tylko dopóki nie zamkniemy hotkeya (dlatego brak sys.exit() na końcu funkcji). Mimo wszystko się przydaje - możemy sobie np. napisać pętlę, która otworzy nam predefiniowane urle (np. wczytane z pliku).
def runie(self,url=''): import win32com.client self.ie=win32com.client.Dispatch("InternetExplorer.Application") navOpenInBackgroundTab = 0x10000 if (str!=''):self.ie.Navigate2(url,navOpenInBackgroundTab) self.ie.Visible=True self.lineEdit.setText('') sys.exit()
2.5 help - samogenerujący się system pomocy
Funkcja help ,,pokazuje'' dodatkowe pole tekstowe, w którym ,,w locie'' generuje opisy skrótów klawiszowych. Jak to robi?
Przez funkcję filematch, z pliku pygrep, znajduje w pliku hotkey.pyw wszystkie linie pasujące do regexpa:
".*str=='([^']*)')[^(]*([^)]*)[^#]*#(.*)"
i wybiera 1 i 2 gruping - czyli pierwszy element między znakami apostrofów i wszystko, co znajduje się za znakiem hash. Jeśli więc będziesz dodawać warunki if w getHotkey sformatowane tak, jak te zaprezentowane, to generator pomocy automatycznie będzie je dodawać.
Pomocnicze funkcje showHelp i hideHelp pozwalają pokazać/ukryć dodatkowe pole tekstowe i zmienić rozmiar okienka hotkeya, nie przesuwając go po ekranie (co, moim zdaniem, nie jest oczywiste w realizacji).
2.6 narzędzia współpracy ze schowkiem - win32clipboard
Funkcja cc w prezentowanej wersji pozwala ,,współpracować'' ze schowkiem przez bibliotekę standardową win32clipboard. Pobierając z niego zawartość i wstawiając ją jako tekst usuwa ze schowka formatowanie. Można sobie to oczywiście rozwinąć o sortowanie zawartości schowka, statstykę tej zawartości itd.
3 Kod programu - całość
3.1 Program główny
Nazwa pliku z programem głównym nie ma znaczenia oprócz tego, że jeśli rozszerzeniem będzie pyw, to nie będzie się otwierało okienko cmd przy każdym uruchomieniu aplikacji. Nazwa pliku z UI to hotkeyui.pyw - ma znaczenie, gdyż jest importowana w programie głównym - jeśli ją zmienisz, to pamietaj to poprawić.
# -*- coding: utf-8 -*- import sys from hotkeyui import * try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s chrome=r'c:Program Files (x86)ChromeApplicationchrome.exe' firefox=r'c:Program Files (x86)Mozilla Firefoxfirefox.exe' class HotkeyL(Ui_MainWindow): """tu powinien byc docstring, ktory pewnie kiedys napisze""" def setupUi2(self): QtGui.QShortcut(QtGui.QKeySequence("Ctrl+w"),self.lineEdit,self.close) QtGui.QShortcut(QtGui.QKeySequence("Esc"),self.lineEdit,self.close) QtCore.QObject.connect(self.lineEdit, QtCore.SIGNAL(_fromUtf8("textChanged(QString)")), self.getHotkey) self.hideHelp() def showHelp(self): self.plainTextEdit.show() pos=MainWindow.pos() x=pos.x() y=pos.y() MainWindow.setMinimumHeight(30) MainWindow.setGeometry(x+50,y+50,300,600) def hideHelp(self): self.plainTextEdit.hide() pos=MainWindow.pos() x=pos.x() y=pos.y() MainWindow.setMinimumHeight(30) MainWindow.setGeometry(x+50,y+50,60,60) def close(self): sys.exit() def help(self): import pygrep self.showHelp() help=[] for x in pygrep.filematch('hotkey.pyw',r".*str=='([^']*)')[^(]*([^)]*)[^#]*#(.*)",[1,2]): help.append(":t".join(x)) self.plainTextEdit.setPlainText("n".join(sorted(help))) self.lineEdit.setText("") def getHotkey(self,str): if (str=='e'):self.run(r"C:/tools/emacs/emacs-24.3/bin/emacs.exe") #run emacs if (str=='1c'):self.cc('clear')#clear text in clpboard if (str=='h'):self.help()#get help if (str=='bc'):self.run(chrome)#run chrome if (str=='bf'):self.run(firefox)#run firefox if (str=='bi'):self.runie()#run IE if (str=='c'):self.statusBar.showMessage("run commandline...")#run commandline if (str=='cc'):self.run("cmd")#cmd if (str=='cp'):self.run("powershell")#powershell if (str=='cA'):self.run("powershell Start-Process powershell -Verb runAs")#admin powershell if (str=='cl'):self.run(["C:/cygwin/bin/mintty.exe","-i", "/Cygwin-Terminal.ico", "-"])#cygwin if (str=='r'):self.statusBar.showMessage("run program...")#run program if (str=='rl'):self.run("C:Program Files (x86)Microsoft Lynccommunicator.exe")#run lync if (str=='rv'):self.run("C:Program FilesOracleVirtualBoxVirtualBox.exe")#run virtualbox if (str=='ro'):self.run(r"C:Program Files (x86)Microsoft OfficeOffice14Outlook.exe")#run outlook def cc(self,mode='clear'): import win32clipboard win32clipboard.OpenClipboard() a=win32clipboard.GetClipboardData() win32clipboard.EmptyClipboard() if(mode=='clear'):win32clipboard.SetClipboardText(a) win32clipboard.CloseClipboard() self.statusBar.showMessage("clipboard %s"%mode) self.lineEdit.setText("1") #sys.exit() def runie(self,url=''): import win32com.client self.ie=win32com.client.Dispatch("InternetExplorer.Application") navOpenInBackgroundTab = 0x10000 if (str!=''):self.ie.Navigate2(url,navOpenInBackgroundTab) self.ie.Visible=True sys.exit() def run(self,*str,cwd=None): from subprocess import Popen Popen( *str ,cwd=cwd) sys.exit() return if __name__ == "__main__": app = QtGui.QApplication(sys.argv) MainWindow = QtGui.QMainWindow() flagi=QtCore.Qt.WindowFlags() flagi |= QtCore.Qt.WindowStaysOnTopHint flagi |=QtCore.Qt.CustomizeWindowHint MainWindow.setWindowFlags(flagi) ui = HotkeyL() ui.setupUi(MainWindow) ui.setupUi2() ui.statusBar.showMessage("start") MainWindow.show() sys.exit(app.exec_())
3.2 UI
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'hotkey.ui' # # Created: Tue Mar 4 10:58:37 2014 # by: PyQt4 UI code generator 4.10.3 # # WARNING! All changes made in this file will be lost! from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: def _fromUtf8(s): return s try: _encoding = QtGui.QApplication.UnicodeUTF8 def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def _translate(context, text, disambig): return QtGui.QApplication.translate(context, text, disambig) class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(_fromUtf8("MainWindow")) MainWindow.resize(127, 135) self.centralwidget = QtGui.QWidget(MainWindow) self.centralwidget.setObjectName(_fromUtf8("centralwidget")) self.gridLayout = QtGui.QGridLayout(self.centralwidget) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.lineEdit = QtGui.QLineEdit(self.centralwidget) self.lineEdit.setObjectName(_fromUtf8("lineEdit")) self.gridLayout.addWidget(self.lineEdit, 0, 0, 1, 1) self.plainTextEdit = QtGui.QPlainTextEdit(self.centralwidget) self.plainTextEdit.setEnabled(True) self.plainTextEdit.setObjectName(_fromUtf8("plainTextEdit")) self.gridLayout.addWidget(self.plainTextEdit, 1, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.statusBar = QtGui.QStatusBar(MainWindow) self.statusBar.setObjectName(_fromUtf8("statusBar")) MainWindow.setStatusBar(self.statusBar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) if __name__ == "__main__": import sys app = QtGui.QApplication(sys.argv) MainWindow = QtGui.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_())
3.3 pygrep.py
import re import sys def match(listtext,regexp,grupy=0): a=re.compile(regexp) out=[] for x in listtext: if(re.match(a,x) is not None): if (type(grupy)==type([])): matche=[] for y in grupy: matche.append(re.match(a,x).group(y)) out.append(matche) else: out.append(re.match(a,x).group(grupy)) return out def filematch(filename,regexp,grupy=0): f=open(filename) #print(f) a=f.readlines() # print(a) f.close() out=match(a,regexp,grupy) return out
4 Podsumowanie
Jeśli chcesz, możesz swobodnie wykorzystać zamieszczony kod. Jest on pozbawiony wszelkiej gwarancji itd, jednak nie powinien nic złego Ci zrobić:) Mam nadzieję, że choć jego fragmenty Ci się przydadzą!
Footnotes:
1 Dlaczego jeśli jest otwarte okno cmd, to z menu shift+pklik znika opcja uruchom jako?