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.

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:

  1. wyświetla malutkie okienko z polem do wprowadzania,
  2. odczytuje wprowadzoną zawartość,
  3. jeżeli zawartość pasuje do zdefiniowanego wzorca, uruchamia procedurę w pythonie i się zamyka,
  4. 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?