Делая графическую обертку для какого-то инструмента wxPython нередко приходится сталкиваться с необходимостью вывода событий (простых логов) в окно wx.TextCtrl. Сегодня поговорим о том, как сделать окно вывода логов.
Особенности создания окна вывода логов
Поставленную задачу можно реализовать с помощью метода wx.TextCtrl.AppendText(msg), добавляющего в окно принятую строку msg к имеющимся.
Сама база, с которой нам придется работать, выглядит вот так.
import logging import logging.config import wx class CustomConsoleHandler(logging.StreamHandler): """""" def __init__(self, textctrl): """""" logging.StreamHandler.__init__(self) self.textctrl = textctrl def emit(self, record): """Constructor""" msg = self.format(record) self.textctrl.WriteText(msg + "\n") self.flush() class MyPanel(wx.Panel): """""" def __init__(self, parent): """Constructor""" wx.Panel.__init__(self, parent) self.logger = logging.getLogger("wxApp") self.logger.info("Test from MyPanel __init__") logText = wx.TextCtrl( self, style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) btn = wx.Button(self, label="Press Me") btn.Bind(wx.EVT_BUTTON, self.onPress) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(logText, 1, wx.EXPAND|wx.ALL, 5) sizer.Add(btn, 0, wx.ALL, 5) self.SetSizer(sizer) txtHandler = CustomConsoleHandler(logText) self.logger.addHandler(txtHandler) def onPress(self, event): """ On the press of a button, log some messages """ self.logger.error("Error Will Robinson!") self.logger.info("Informational message") class MyFrame(wx.Frame): """""" def __init__(self): """Constructor""" wx.Frame.__init__(self, None, title="Logging test") panel = MyPanel(self) self.logger = logging.getLogger("wxApp") self.Show() def main(): """ Запускаем программу """ dictLogConfig = { "version":1, "handlers":{ "fileHandler":{ "class":"logging.FileHandler", "formatter":"myFormatter", "filename":"test.log" }, "consoleHandler":{ "class":"logging.StreamHandler", "formatter":"myFormatter" } }, "loggers":{ "wxApp":{ "handlers":["fileHandler", "consoleHandler"], "level":"INFO", } }, "formatters":{ "myFormatter":{ "format":"%(asctime)s - %(name)s - %(levelname)s - %(message)s" } } } logging.config.dictConfig(dictLogConfig) logger = logging.getLogger("wxApp") logger.info("This message came from main!") app = wx.App(False) frame = MyFrame() app.MainLoop() if __name__ == "__main__": main()
Как видим, здесь использовался модуль logging.config. А метод dictConfig был добавлен в Python 2.7. Простыми словами настраивается хэндлер логгинга и форматтеры, и все, что не находится в словаре. Затем все это пропускается через logging.config.
Если вы попробуете выполнить данный код, то обнаружите, что первые несколько сообщений отправляются в stdout и в лог, но не в текстовый контроль. В конце панельного класса __init__ добавляется стандартный хэндлер, и здесь осуществляется перенаправление сообщений логгинга в текстовый контроль.
Вот как это будет выглядеть на компьютере под управлением Mac.
Вы обнаружите, что можете увидеть данные о логгинге в stdout, а также в текстовом контроле на скриншоте.
В терминале и файле лога должна появиться информация такого типа.
2016-11-30 14:04:35,026 — wxApp — INFO — This message came from main!
2 2016-11-30 14:04:35,026 — wxApp — INFO — Test from MyPanel __init__
3 2016-11-30 14:04:38,261 — wxApp — ERROR — Error Will Robinson!
Одним словом, это база того, как перенаправлять методы логгинга Python в виджет wxPython. Это может быть очень полезным в случае, если надо сохранить выходные данные из приложения в отдельный файл, а не только лишь просматривать их в режиме реального времени. А теперь поговорим о том, как перенаправить stdout в wx.TextCtrl.
Перенаправление stdout в wx.Text.Ctrl
Давайте возьмем frame, который указан ранее и удалим класс CustomConsoleHandler, а также весь код, который с ним связан.
import wx import random class MyPanel(wx.Panel): def __init__(self, parent): """Constructor""" # создадим панель в которую помести окно вывода и кнопку для генерации событий wx.Panel.__init__(self, parent) self.logText = wx.TextCtrl( self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.TE_RICH) btn = wx.Button(self, label="Press Me") btn.Bind(wx.EVT_BUTTON, self.onPress) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.logText, 1, wx.EXPAND | wx.ALL, 5) sizer.Add(btn, 0, wx.ALL, 5) self.SetSizer(sizer) def onPress(self, event): """ Ивент который генерирует сообщение по нажатию на кнопку """ random_list = ['ERROR: this is error\n', 'INFO: start program\n', 'DEBUG: debug info for developer\n'] self.outputPrint(random.choice(random_list)) def outputPrint(self, message): self.logText.AppendText(message) class MyFrame(wx.Frame): def __init__(self): """Constructor""" wx.Frame.__init__(self, None, title="Logging test") panel = MyPanel(self) self.Show()
Теперь добавим метод, который будет выводить в окно логов определенный текст по нажатию кнопки.
Здесь важно помнить, что после message необходимо добавить \n, чтобы перенести новую строку логов. Как следствие, получаем такой результат в окне вывода.
Есть также возможность инвертировать лог путем добавления чекбокса wx.CheckBox и создав список, в котором можно хранить все сообщения.
Тем не менее есть определенные сложности. Перед каждым вызовом метода надо осуществлять очистку TextCtrl немного по-другому. В ином случае сообщения будут дублироваться и спамить.
А после цикла необходимо передвинуть курсор вверх на последнее сообщение инвертированного лога. И здесь на помощь придет wx.TextCtrl.SetInsertionPoint(0), помещающий курсор TextCtrl в требуемое положение.
А для инверсии необходимо проверить флажок с использованием метода IsChecked. В случае, если он равняется True, необходимо выполнить reversed имеющегося списка с сообщениями. Следовательно, будет осуществлено инвертирование вывода.
# coding: utf-8 import wx import random class MyPanel(wx.Panel): def __init__(self, parent): """Constructor""" # создадим панель в которую поместить окно вывода, чебокс инвертировать и кнопку для генерации событий wx.Panel.__init__(self, parent) # здесь мы будем хранить все сообщения self.cache_msg = list() self.logText = wx.TextCtrl( self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.TE_RICH) btn = wx.Button(self, label="Press Me") btn.Bind(wx.EVT_BUTTON, self.onPress) # чебокс self.check_box_invert = wx.CheckBox(self, wx.ID_ANY, 'Invert log', wx.DefaultPosition, wx.DefaultSize, 0) sizer = wx.BoxSizer(wx.VERTICAL) # горизонтальный сайзер в который мы поместим кнопку и чекбокс, чтобы они были рядом hor_sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.logText, 1, wx.EXPAND | wx.ALL, 5) hor_sizer.Add(btn, 0, wx.ALL, 5) hor_sizer.Add(self.check_box_invert, 0, wx.ALL, 5) sizer.Add(hor_sizer, 0, wx.ALL, 5) self.SetSizer(sizer) def onPress(self, event): """ Ивент который генерирует сообщение по нажатию на кнопку """ random_list = ['ERROR: this is error\n', 'INFO: start program\n', 'DEBUG: debug info for developer\n'] self.outputPrint(random.choice(random_list)) def outputPrint(self, message): # сохраним наше сообщение в списке self.cache_msg.append(message) # очистим окно от старых сообщений self.logText.Clear() # в зависимости от состояния флажка одном сообщения в окне # если флажок установлен, то инвертируем лог (последнее сообщение будет вверху) if self.check_box_invert.IsChecked(): for msg in reversed(self.cache_msg): # определим цвет сообщения color = self.outputColored(str(msg)) # добавим его в окно self.logText.AppendText(msg) # окрасим вывод self.logText.SetForegroundColour(color) # перемещаем курсор в верхнее положение, иначе пользователь будет видеть нижнее сообщение # используем только если инвертировать лог включено self.logText.SetInsertionPoint(0) else: for msg in self.cache_msg: color = self.outputColored(str(msg)) self.logText.AppendText(msg) self.logText.SetForegroundColour(color) @staticmethod # метод определяет цвет вывода def outputColored(message): return wx.RED if message.split()[0] == 'ERROR:' else wx.BLACK class MyFrame(wx.Frame): def __init__(self): """Constructor""" wx.Frame.__init__(self, None, title="Logging test") panel = MyPanel(self) self.Show() def main(): app = wx.App(False) frame = MyFrame() app.MainLoop() if __name__ == "__main__": main()
Помимо этого, есть возможность сделать вывод подходящего цвета в случае попадания в лог какого-то конкретного сообщения. Чтобы продемонстрировать это, добавим random и сгенерируем список событий, которые будут создавать наш лог. Также нужно будет использовать метод, определяющий цвет лога.
Учтите то, что в этой реализации окрашиваться будет вывод полностью, а не определенная строчка. Но это легко исправляется путем изменения вывода.
Нюанс
Если логи будут приходить не из потока, а из процесса в бэкенде либо в ui, то само приложение может остановиться, ожидая ответа. Следовательно, данный метод может использоваться для логирования внутренних методов самого ui либо вынесения логов в отдельный поток.
Выводы
Чтобы вести маленькое количество логов, можно использовать AppendText. В ином случае необходимо подумать по поводу хендлера, поскольку описанный ранее метод – далеко не самый идеальный. Вывод в окне может быть окрашен любым цветом. Можно использовать как цвета от самого wx, но и от множества RGB.