Wednesday, March 11, 2015

Winforms with Python




Winforms with Python





Overview

When developing GUI applications in python for Windows, standard python library choices are limiting and do not provide the same quality of experience as applications developed with Visual Studio, especially when bundled together with 3rd party WinForms or WPF such as DevExpress,  As a result, python developers typically turn to Visual Studio and IronPython when developing for Windows.  However, there is an alternative.


Python.Net

Python.Net(http://pythonnet.sourceforge.net/), built by Brian Lloyd is a lightweight, yet powerful package that enables CPython to load and interact with CLR based packages/libraries.   It is fairly robust, performant and supports most CLR constructs and data types.  In the simplest form, the following coding snippet demonstrates power and capability of Python.Net.  You can download Python.Net here(http://www.lfd.uci.edu/~gohlke/pythonlibs/)

Type "help", "copyright", "credits" or "license" for more information.
>>> import clr
>>> import System
>>> clrStr = System.String("I am a CLR Object")
>>> clrStr
<System.String object at 0x0000000002B76518>
>>> clrStr.ToString()
'I am a CLR Object'


Building a WinForms application

    With python.net configured, we can now build a basic Winforms application running from CPython.  First let’s use Visual Studio to build a basic form and compile it into an clr assembly.   For below example, I created a form called SimpleForm in SimpleForm namespace and packaged it inside SimpleForm.dll. The form has text field called textBox and a button called btn_sayit.    The following python code can then be used launch the form and handle click event on btn_sayit to print contents of textBox to the console.

import clr
from System.Reflection import Assembly
from System.Windows.Forms import Application
Assembly.LoadFile(r'c:\work\simple_form\SimpleForm.DLL')
import SimpleForm

form = SimpleForm.SimpleForm()

def event_handler(sender, args):
   print("Got Text: %s" % form.textBox.Text)
form.btn_sayit.Click += event_handler

Application.Run(form)

To simplify the python interface, I’ve set Modifier as public on textBox and btn_sayit widgets, this is not strictly necessary as you can also get access to them using form.Controls collection, but it makes for simpler and cleaner interface


Defining a UI Framework

To keep GUI Development more pythonic and UnitTestable, we can formally define Model View and Presenter python classes which would decouple WinForms code from the application and create a classical UI segregation of responsibilities.

import clr
from System.Reflection import Assembly
from System.Windows.Forms import Application
Assembly.LoadFile(r'c:\work\simple_form\SimpleForm.DLL')
import SimpleForm

class View(SimpleForm.SimpleForm):
   @property
   def text(self):
       return self.textBox.Text

   def bind_handler(self, handler):
       self.btn_sayit.Click += handler

class Presenter:
   def __init__(self, view, model):
       self.view = view
       self.model = model
       self.bind_events()

   def bind_events(self):
       self.view.bind_handler(self.sayit_handler)
   
   def sayit_handler(self, sender, args):
       self.model.perform_sayit(self.view.text)


class Model:
   def perform_sayit(self, text):
       print("Got Text: %s" % text)

p = Presenter(View(), Model())
Application.Run(p.view)


With above structure in place, all business logic is performed in the model that is completely independent of the view and, therefore, can be unit tested and developed separately from WinForms components.  Furthermore, presenter is injected with a python object wrapping a WinForms view which can be easily substituted with a stub or a mock for unit testing purposes.

Above pattern is an extension of a Passive View Pattern described by Martin Fowler  http://martinfowler.com/eaaDev/PassiveScreen.html



Handling real-time updates

Most modern UIs also update based on some external events, not just user actions.  To support that, we need to extend our model and include an event mechanism that will flow events from python to the WinForms layer.   In the previous section, when we invoked Application.Run() on the view, we’ve effectively handed control over the the windows message loop(http://en.wikipedia.org/wiki/Message_loop_in_Microsoft_Windows) and any further updates must be performed from a python thread that should have been started before the Application.Run() has been called. Furthermore, WinForms are not thread-safe, therefore one must use Invoke or BeginInvoke to update the UI.  

import clr
from System.Reflection import Assembly
from System.Windows.Forms import Application
from System.Windows.Forms import MethodInvoker

Assembly.LoadFile(r'c:\work\simple_form\SimpleForm.DLL')
import SimpleForm
import threading
import random
import time


class View(SimpleForm.SimpleForm):
   @property
   def text(self):
       return self.textBox.Text
   @text.setter
   def text(self, value):
       def updater():
           self.textBox.Text = str(value)
       self.BeginInvoke(MethodInvoker(updater))
       
   def bind_handler(self, handler):
       self.btn_sayit.Click += handler


class Presenter:
   def __init__(self, view, model, event_generator):
       self.view = view
       self.model = model
       self.event_generator = event_generator
       self.bind_events()

   def bind_events(self):
       self.view.bind_handler(self.sayit_handler)
       self.event_generator.bind_handler(self.update_text)
   
   def update_text(self, new_text):
       self.view.text = new_text
       
   def sayit_handler(self, sender, args):
       self.model.perform_sayit(self.view.text)


class Model:
   def perform_sayit(self, text):
       print("Got Text: %s" % text)


class EventGenerator(threading.Thread):
   _handler = None

   def bind_handler(self, handler):
       self._handler = handler
   
   def run(self):
       while True:
           if self._handler:
               self._handler( random.random() )
           time.sleep( 1 )


generator = EventGenerator()
generator.setDaemon(True)
generator.start()
p = Presenter(View(), Model(), generator)
Application.Run(p.view)

No comments: