Reputation: 45008
I'm using a module in my python app that writes a lot a of messages using the logging module. Initially I was using this in a console application and it was pretty easy to get the logging output to display on the console using a console handler. Now I've developed a GUI version of my app using wxPython and I'd like to display all the logging output to a custom control — a multi-line textCtrl. Is there a way i could create a custom logging handler so i can redirect all the logging output there and display the logging messages wherever/however I want — in this case, a wxPython app.
Upvotes: 12
Views: 12002
Reputation: 21
Answer
A general solution for custom log routing might look something like this:
import logging
class CustomLogHandler(logging.Handler):
def __init__(self, handler: "function"):
logging.Handler.__init__(self)
self.handler = handler
def emit(self, record)
record = self.format(record)
self.handler(record)
Exmaple
This custom handler would take in a function that would be called with the log message every time a logger using this handler logs a message. This means you can do something like this
import logging
def myLogHandler(self, logMSG):
print(logMSG)
logObj = logging.getLogger(name) # Name of your logger here (or root)
myCustHandler = CustomLogHandler(myLogHandler)
logObj.addHandler(myCustHandler)
myLogHandler
would be called every time you log a message with logObj
. As I wrote it, this would just print the log message to the console.
In that function, you could set up a way to route that log message to your GUI and get the desired output.
Why does this not answer the question directly about wxPython?
I wanted to provide a general solution that was framework-agnostic.
The handler class I wrote could still be used if you switched from wxPython to PyQt or Tkinter or any other framework/library. All that would have to change is the handler function. That makes this code much more reusable.
Other Relevant Topics
For anyone reading this solution: If you are not sure why this code is more reusable research some topics like Dependency Injection and SOLID Design Principles if you are unfamiliar with them. After reading a bit about those, the motivation for this particular design might become clearer.
Upvotes: 0
Reputation: 99365
Here's a simple working example:
import logging
import random
import sys
import wx
logger = logging.getLogger(__name__)
class WxTextCtrlHandler(logging.Handler):
def __init__(self, ctrl):
logging.Handler.__init__(self)
self.ctrl = ctrl
def emit(self, record):
s = self.format(record) + '\n'
wx.CallAfter(self.ctrl.WriteText, s)
LEVELS = [
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.CRITICAL
]
class Frame(wx.Frame):
def __init__(self):
TITLE = "wxPython Logging To A Control"
wx.Frame.__init__(self, None, wx.ID_ANY, TITLE)
panel = wx.Panel(self, wx.ID_ANY)
log = wx.TextCtrl(panel, wx.ID_ANY, size=(300,100),
style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL)
btn = wx.Button(panel, wx.ID_ANY, 'Log something!')
self.Bind(wx.EVT_BUTTON, self.onButton, btn)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(log, 1, wx.ALL|wx.EXPAND, 5)
sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5)
panel.SetSizer(sizer)
handler = WxTextCtrlHandler(log)
logger.addHandler(handler)
FORMAT = "%(asctime)s %(levelname)s %(message)s"
handler.setFormatter(logging.Formatter(FORMAT))
logger.setLevel(logging.DEBUG)
def onButton(self, event):
logger.log(random.choice(LEVELS), "More? click again!")
if __name__ == "__main__":
app = wx.PySimpleApp()
frame = Frame().Show()
app.MainLoop()
Screenshot:
Update: As iondiode points out, this simple script may have problems if there are multiple threads in your app, all logging via such a handler; ideally only a UI thread should update the UI. You can use the suggested approach of logging the event by using a custom event, as per his answer.
Upvotes: 6
Reputation: 650
Create Handler
import wx
import wx.lib.newevent
import logging
# create event type
wxLogEvent, EVT_WX_LOG_EVENT = wx.lib.newevent.NewEvent()
class wxLogHandler(logging.Handler):
"""
A handler class which sends log strings to a wx object
"""
def __init__(self, wxDest=None):
"""
Initialize the handler
@param wxDest: the destination object to post the event to
@type wxDest: wx.Window
"""
logging.Handler.__init__(self)
self.wxDest = wxDest
self.level = logging.DEBUG
def flush(self):
"""
does nothing for this handler
"""
def emit(self, record):
"""
Emit a record.
"""
try:
msg = self.format(record)
evt = wxLogEvent(message=msg,levelname=record.levelname)
wx.PostEvent(self.wxDest,evt)
except (KeyboardInterrupt, SystemExit):
raise
except:
self.handleError(record)
Then in your control
self.Bind(EVT_WX_LOG_EVENT, self.onLogEvent)
def onLogEvent(self,event):
'''
Add event.message to text window
'''
msg = event.message.strip("\r")+"\n"
self.logwindow.AppendText(msg) # or whatevery
event.Skip()
Upvotes: 14
Reputation: 83250
You will need to create a custom logging.Handler
and add it to your logging.Logger
.
From the documentation:
Handler
objects are responsible for dispatching the appropriate log messages (based on the log messages’ severity) to the handler’s specified destination. Logger objects can add zero or more handler objects to themselves with an addHandler() method. As an example scenario, an application may want to send all log messages to a log file, all log messages of error or higher to stdout, and all messages of critical to an email address. This scenario requires three individual handlers where each handler is responsible for sending messages of a specific severity to a specific location.
See http://docs.python.org/library/logging.html#handler-objects for the Handler
API.
In particular, it is the Handler.emit(record)
method that you can implement to specify the destination of the output. Presumably, you would implement this to call TextCtrl.AppendText
.
Upvotes: 3