Делая графическую обертку для какого-то инструмента 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.















