diff --git a/IPython/frontend/qt/base_frontend_mixin.py b/IPython/frontend/qt/base_frontend_mixin.py
new file mode 100644
index 0000000..3eaa759
--- /dev/null
+++ b/IPython/frontend/qt/base_frontend_mixin.py
@@ -0,0 +1,85 @@
+""" Defines a convenient mix-in class for implementing Qt frontends.
+"""
+
+class BaseFrontendMixin(object):
+    """ A mix-in class for implementing Qt frontends.
+
+    To handle messages of a particular type, frontends need only define an
+    appropriate handler method. For example, to handle 'stream' messaged, define
+    a '_handle_stream(msg)' method.
+    """
+
+    #---------------------------------------------------------------------------
+    # 'BaseFrontendMixin' concrete interface
+    #---------------------------------------------------------------------------
+    
+    def _get_kernel_manager(self):
+        """ Returns the current kernel manager.
+        """
+        return self._kernel_manager
+
+    def _set_kernel_manager(self, kernel_manager):
+        """ Disconnect from the current kernel manager (if any) and set a new
+            kernel manager.
+        """
+        # Disconnect the old kernel manager, if necessary.
+        old_manager = self._kernel_manager
+        if old_manager is not None:
+            old_manager.started_channels.disconnect(self._started_channels)
+            old_manager.stopped_channels.disconnect(self._stopped_channels)
+
+            # Disconnect the old kernel manager's channels.
+            old_manager.sub_channel.message_received.disconnect(self._dispatch)
+            old_manager.xreq_channel.message_received.disconnect(self._dispatch)
+            old_manager.rep_channel.message_received.connect(self._dispatch)
+
+            # Handle the case where the old kernel manager is still listening.
+            if old_manager.channels_running:
+                self._stopped_channels()
+
+        # Set the new kernel manager.
+        self._kernel_manager = kernel_manager
+        if kernel_manager is None:
+            return
+
+        # Connect the new kernel manager.
+        kernel_manager.started_channels.connect(self._started_channels)
+        kernel_manager.stopped_channels.connect(self._stopped_channels)
+
+        # Connect the new kernel manager's channels.
+        kernel_manager.sub_channel.message_received.connect(self._dispatch)
+        kernel_manager.xreq_channel.message_received.connect(self._dispatch)
+        kernel_manager.rep_channel.message_received.connect(self._dispatch)
+        
+        # Handle the case where the kernel manager started channels before
+        # we connected.
+        if kernel_manager.channels_running:
+            self._started_channels()
+
+    kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
+
+    #---------------------------------------------------------------------------
+    # 'BaseFrontendMixin' abstract interface
+    #---------------------------------------------------------------------------
+    
+    def _started_channels(self):
+        """ Called when the KernelManager channels have started listening or 
+            when the frontend is assigned an already listening KernelManager.
+        """
+
+    def _stopped_channels(self):
+        """ Called when the KernelManager channels have stopped listening or
+            when a listening KernelManager is removed from the frontend.
+        """
+
+    #---------------------------------------------------------------------------
+    # Private interface
+    #---------------------------------------------------------------------------
+
+    def _dispatch(self, msg):
+        """ Call the frontend handler associated with
+        """
+        msg_type = msg['msg_type']
+        handler = getattr(self, '_handle_' + msg_type, None)
+        if handler:
+            handler(msg)
diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py
index e16eb0b..7431e30 100644
--- a/IPython/frontend/qt/console/frontend_widget.py
+++ b/IPython/frontend/qt/console/frontend_widget.py
@@ -9,6 +9,7 @@ import zmq
 
 # Local imports
 from IPython.core.inputsplitter import InputSplitter
+from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
 from call_tip_widget import CallTipWidget
 from completion_lexer import CompletionLexer
 from console_widget import HistoryConsoleWidget
@@ -60,7 +61,7 @@ class FrontendHighlighter(PygmentsHighlighter):
         PygmentsHighlighter.setFormat(self, start, count, format)
 
 
-class FrontendWidget(HistoryConsoleWidget):
+class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
     """ A Qt frontend for a generic Python kernel.
     """
    
@@ -146,70 +147,96 @@ class FrontendWidget(HistoryConsoleWidget):
         return not self._complete()
 
     #---------------------------------------------------------------------------
-    # 'FrontendWidget' interface
+    # 'BaseFrontendMixin' abstract interface
     #---------------------------------------------------------------------------
 
-    def execute_file(self, path, hidden=False):
-        """ Attempts to execute file with 'path'. If 'hidden', no output is
-            shown.
+    def _handle_complete_reply(self, rep):
+        """ Handle replies for tab completion.
         """
-        self.execute('execfile("%s")' % path, hidden=hidden)
+        cursor = self._get_cursor()
+        if rep['parent_header']['msg_id'] == self._complete_id and \
+                cursor.position() == self._complete_pos:
+            text = '.'.join(self._get_context())
+            cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
+            self._complete_with_items(cursor, rep['content']['matches'])
 
-    def _get_kernel_manager(self):
-        """ Returns the current kernel manager.
+    def _handle_execute_reply(self, msg):
+        """ Handles replies for code execution.
         """
-        return self._kernel_manager
+        if not self._hidden:
+            # Make sure that all output from the SUB channel has been processed
+            # before writing a new prompt.
+            self.kernel_manager.sub_channel.flush()
+
+            content = msg['content']
+            status = content['status']
+            if status == 'ok':
+                self._process_execute_ok(msg)
+            elif status == 'error':
+                self._process_execute_error(msg)
+            elif status == 'abort':
+                self._process_execute_abort(msg)
+
+            self._hidden = True
+            self._show_interpreter_prompt()
+            self.executed.emit(msg)
+
+    def _handle_input_request(self, msg):
+        """ Handle requests for raw_input.
+        """
+        # Make sure that all output from the SUB channel has been processed
+        # before entering readline mode.
+        self.kernel_manager.sub_channel.flush()
+
+        def callback(line):
+            self.kernel_manager.rep_channel.input(line)
+        self._readline(msg['content']['prompt'], callback=callback)
 
-    def _set_kernel_manager(self, kernel_manager):
-        """ Disconnect from the current kernel manager (if any) and set a new
-            kernel manager.
+    def _handle_object_info_reply(self, rep):
+        """ Handle replies for call tips.
         """
-        # Disconnect the old kernel manager, if necessary.
-        if self._kernel_manager is not None:
-            self._kernel_manager.started_channels.disconnect(
-                self._started_channels)
-            self._kernel_manager.stopped_channels.disconnect(
-                self._stopped_channels)
-
-            # Disconnect the old kernel manager's channels.
-            sub = self._kernel_manager.sub_channel
-            xreq = self._kernel_manager.xreq_channel
-            rep = self._kernel_manager.rep_channel
-            sub.message_received.disconnect(self._handle_sub)
-            xreq.execute_reply.disconnect(self._handle_execute_reply)
-            xreq.complete_reply.disconnect(self._handle_complete_reply)
-            xreq.object_info_reply.disconnect(self._handle_object_info_reply)
-            rep.input_requested.disconnect(self._handle_req)
-
-            # Handle the case where the old kernel manager is still listening.
-            if self._kernel_manager.channels_running:
-                self._stopped_channels()
-
-        # Set the new kernel manager.
-        self._kernel_manager = kernel_manager
-        if kernel_manager is None:
-            return
+        cursor = self._get_cursor()
+        if rep['parent_header']['msg_id'] == self._call_tip_id and \
+                cursor.position() == self._call_tip_pos:
+            doc = rep['content']['docstring']
+            if doc:
+                self._call_tip_widget.show_docstring(doc)
 
-        # Connect the new kernel manager.
-        kernel_manager.started_channels.connect(self._started_channels)
-        kernel_manager.stopped_channels.connect(self._stopped_channels)
-
-        # Connect the new kernel manager's channels.
-        sub = kernel_manager.sub_channel
-        xreq = kernel_manager.xreq_channel
-        rep = kernel_manager.rep_channel
-        sub.message_received.connect(self._handle_sub)
-        xreq.execute_reply.connect(self._handle_execute_reply)
-        xreq.complete_reply.connect(self._handle_complete_reply)
-        xreq.object_info_reply.connect(self._handle_object_info_reply)
-        rep.input_requested.connect(self._handle_req)
-        
-        # Handle the case where the kernel manager started channels before
-        # we connected.
-        if kernel_manager.channels_running:
-            self._started_channels()
+    def _handle_pyout(self, msg):
+        """ Handle display hook output.
+        """
+        self._append_plain_text(msg['content']['data'] + '\n')
 
-    kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
+    def _handle_stream(self, msg):
+        """ Handle stdout, stderr, and stdin.
+        """
+        self._append_plain_text(msg['content']['data'])
+        self._control.moveCursor(QtGui.QTextCursor.End)
+    
+    def _started_channels(self):
+        """ Called when the KernelManager channels have started listening or 
+            when the frontend is assigned an already listening KernelManager.
+        """
+        self._reset()
+        self._append_plain_text(self._get_banner())
+        self._show_interpreter_prompt()
+
+    def _stopped_channels(self):
+        """ Called when the KernelManager channels have stopped listening or
+            when a listening KernelManager is removed from the frontend.
+        """
+        # FIXME: Print a message here?
+        pass
+
+    #---------------------------------------------------------------------------
+    # 'FrontendWidget' interface
+    #---------------------------------------------------------------------------
+
+    def execute_file(self, path, hidden=False):
+        """ Attempts to execute file with 'path'. If 'hidden', no output is
+            shown.
+        """
+        self.execute('execfile("%s")' % path, hidden=hidden)
 
     #---------------------------------------------------------------------------
     # 'FrontendWidget' protected interface
@@ -275,26 +302,32 @@ class FrontendWidget(HistoryConsoleWidget):
             self._append_plain_text('Kernel process is either remote or '
                                     'unspecified. Cannot interrupt.\n')
 
-    def _show_interpreter_prompt(self):
-        """ Shows a prompt for the interpreter.
+    def _process_execute_abort(self, msg):
+        """ Process a reply for an aborted execution request.
         """
-        self._show_prompt('>>> ')
-
-    #------ Signal handlers ----------------------------------------------------
+        self._append_plain_text("ERROR: execution aborted\n")
 
-    def _started_channels(self):
-        """ Called when the kernel manager has started listening.
+    def _process_execute_error(self, msg):
+        """ Process a reply for an execution request that resulted in an error.
         """
-        self._reset()
-        self._append_plain_text(self._get_banner())
-        self._show_interpreter_prompt()
+        content = msg['content']
+        traceback = ''.join(content['traceback'])
+        self._append_plain_text(traceback)
 
-    def _stopped_channels(self):
-        """ Called when the kernel manager has stopped listening.
+    def _process_execute_ok(self, msg):
+        """ Process a reply for a successful execution equest.
         """
-        # FIXME: Print a message here?
+        # The basic FrontendWidget doesn't handle payloads, as they are a
+        # mechanism for going beyond the standard Python interpreter model.
         pass
 
+    def _show_interpreter_prompt(self):
+        """ Shows a prompt for the interpreter.
+        """
+        self._show_prompt('>>> ')
+
+    #------ Signal handlers ----------------------------------------------------
+
     def _document_contents_change(self, position, removed, added):
         """ Called whenever the document's content changes. Display a call tip
             if appropriate.
@@ -305,72 +338,3 @@ class FrontendWidget(HistoryConsoleWidget):
         document = self._control.document()
         if position == self._get_cursor().position():
             self._call_tip()
-
-    def _handle_req(self, req):
-        # Make sure that all output from the SUB channel has been processed
-        # before entering readline mode.
-        self.kernel_manager.sub_channel.flush()
-
-        def callback(line):
-            self.kernel_manager.rep_channel.input(line)
-        self._readline(req['content']['prompt'], callback=callback)
-
-    def _handle_sub(self, omsg):
-        if self._hidden:
-            return
-        handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
-        if handler is not None:
-            handler(omsg)
-
-    def _handle_pyout(self, omsg):
-        self._append_plain_text(omsg['content']['data'] + '\n')
-
-    def _handle_stream(self, omsg):
-        self._append_plain_text(omsg['content']['data'])
-        self._control.moveCursor(QtGui.QTextCursor.End)
-        
-    def _handle_execute_reply(self, reply):
-        if self._hidden:
-            return
-
-        # Make sure that all output from the SUB channel has been processed
-        # before writing a new prompt.
-        self.kernel_manager.sub_channel.flush()
-
-        content = reply['content']
-        status = content['status']
-        if status == 'ok':
-            self._handle_execute_payload(content['payload'])
-        elif status == 'error':
-            self._handle_execute_error(reply)
-        elif status == 'aborted':
-            text = "ERROR: ABORTED\n"
-            self._append_plain_text(text)
-
-        self._hidden = True
-        self._show_interpreter_prompt()
-        self.executed.emit(reply)
-
-    def _handle_execute_error(self, reply):
-        content = reply['content']
-        traceback = ''.join(content['traceback'])
-        self._append_plain_text(traceback)
-
-    def _handle_execute_payload(self, payload):
-        pass
-
-    def _handle_complete_reply(self, rep):
-        cursor = self._get_cursor()
-        if rep['parent_header']['msg_id'] == self._complete_id and \
-                cursor.position() == self._complete_pos:
-            text = '.'.join(self._get_context())
-            cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
-            self._complete_with_items(cursor, rep['content']['matches'])
-
-    def _handle_object_info_reply(self, rep):
-        cursor = self._get_cursor()
-        if rep['parent_header']['msg_id'] == self._call_tip_id and \
-                cursor.position() == self._call_tip_pos:
-            doc = rep['content']['docstring']
-            if doc:
-                self._call_tip_widget.show_docstring(doc)
diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py
index 127fc56..e938fe5 100644
--- a/IPython/frontend/qt/console/ipython_widget.py
+++ b/IPython/frontend/qt/console/ipython_widget.py
@@ -49,6 +49,18 @@ class IPythonWidget(FrontendWidget):
         self.reset_styling()
 
     #---------------------------------------------------------------------------
+    # 'BaseFrontendMixin' abstract interface
+    #---------------------------------------------------------------------------
+
+    def _handle_pyout(self, msg):
+        """ Reimplemented for IPython-style "display hook".
+        """
+        self._append_html(self._make_out_prompt(self._prompt_count))
+        self._save_prompt_block()
+        
+        self._append_plain_text(msg['content']['data'] + '\n')
+
+    #---------------------------------------------------------------------------
     # 'FrontendWidget' interface
     #---------------------------------------------------------------------------
 
@@ -66,6 +78,21 @@ class IPythonWidget(FrontendWidget):
         """
         return default_banner
 
+    def _process_execute_error(self, msg):
+        """ Reimplemented for IPython-style traceback formatting.
+        """
+        content = msg['content']
+        traceback_lines = content['traceback'][:]
+        traceback = ''.join(traceback_lines)
+        traceback = traceback.replace(' ', ' ')
+        traceback = traceback.replace('\n', '<br/>')
+
+        ename = content['ename']
+        ename_styled = '<span class="error">%s</span>' % ename
+        traceback = traceback.replace(ename, ename_styled)
+
+        self._append_html(traceback)
+
     def _show_interpreter_prompt(self):
         """ Reimplemented for IPython-style prompts.
         """
@@ -93,31 +120,6 @@ class IPythonWidget(FrontendWidget):
         self._set_continuation_prompt(
             self._make_continuation_prompt(self._prompt), html=True)
 
-    #------ Signal handlers ----------------------------------------------------
-
-    def _handle_execute_error(self, reply):
-        """ Reimplemented for IPython-style traceback formatting.
-        """
-        content = reply['content']
-        traceback_lines = content['traceback'][:]
-        traceback = ''.join(traceback_lines)
-        traceback = traceback.replace(' ', '&nbsp;')
-        traceback = traceback.replace('\n', '<br/>')
-
-        ename = content['ename']
-        ename_styled = '<span class="error">%s</span>' % ename
-        traceback = traceback.replace(ename, ename_styled)
-
-        self._append_html(traceback)
-
-    def _handle_pyout(self, omsg):
-        """ Reimplemented for IPython-style "display hook".
-        """
-        self._append_html(self._make_out_prompt(self._prompt_count))
-        self._save_prompt_block()
-        
-        self._append_plain_text(omsg['content']['data'] + '\n')
-
     #---------------------------------------------------------------------------
     # 'IPythonWidget' interface
     #---------------------------------------------------------------------------
diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py
index ff5a831..377b0f6 100644
--- a/IPython/frontend/qt/console/rich_ipython_widget.py
+++ b/IPython/frontend/qt/console/rich_ipython_widget.py
@@ -55,9 +55,10 @@ class RichIPythonWidget(IPythonWidget):
     # 'FrontendWidget' protected interface
     #---------------------------------------------------------------------------
 
-    def _handle_execute_payload(self, payload):
-        """ Reimplemented to handle pylab plot payloads.
+    def _process_execute_ok(self, msg):
+        """ Reimplemented to handle matplotlib plot payloads.
         """
+        payload = msg['content']['payload']
         plot_payload = payload.get('plot', None)
         if plot_payload and plot_payload['format'] == 'svg':
             svg = plot_payload['data']
@@ -73,7 +74,7 @@ class RichIPythonWidget(IPythonWidget):
                 cursor.insertImage(format)
                 cursor.insertBlock()
         else:
-            super(RichIPythonWidget, self)._handle_execute_payload(payload)
+            super(RichIPythonWidget, self)._process_execute_ok(msg)
 
     #---------------------------------------------------------------------------
     # 'RichIPythonWidget' protected interface
diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py
index ee3ba64..f19e48e 100644
--- a/IPython/frontend/qt/kernelmanager.py
+++ b/IPython/frontend/qt/kernelmanager.py
@@ -26,11 +26,21 @@ class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
     # Emitted when any message is received.
     message_received = QtCore.pyqtSignal(object)
 
-    # Emitted when a message of type 'pyout' or 'stdout' is received.
-    output_received = QtCore.pyqtSignal(object)
+    # Emitted when a message of type 'stream' is received.
+    stream_received = QtCore.pyqtSignal(object)
 
-    # Emitted when a message of type 'pyerr' or 'stderr' is received.
-    error_received = QtCore.pyqtSignal(object)
+    # Emitted when a message of type 'pyin' is received.
+    pyin_received = QtCore.pyqtSignal(object)
+
+    # Emitted when a message of type 'pyout' is received.
+    pyout_received = QtCore.pyqtSignal(object)
+
+    # Emitted when a message of type 'pyerr' is received.
+    pyerr_received = QtCore.pyqtSignal(object)
+
+    # Emitted when a crash report message is received from the kernel's
+    # last-resort sys.excepthook.
+    crash_received = QtCore.pyqtSignal(object)
 
     #---------------------------------------------------------------------------
     # 'object' interface
@@ -54,10 +64,11 @@ class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
         
         # Emit signals for specialized message types.
         msg_type = msg['msg_type']
-        if msg_type in ('pyout', 'stdout'):
-            self.output_received.emit(msg)
-        elif msg_type in ('pyerr', 'stderr'):
-            self.error_received.emit(msg)
+        signal = getattr(self, msg_type + '_received', None)
+        if signal:
+            signal.emit(msg)
+        elif msg_type in ('stdout', 'stderr'):
+            self.stream_received.emit(msg)
 
     def flush(self):
         """ Reimplemented to ensure that signals are dispatched immediately.
@@ -136,6 +147,7 @@ class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
         if msg_type == 'input_request':
             self.input_requested.emit(msg)
 
+
 class QtKernelManager(KernelManager, QtCore.QObject):
     """ A KernelManager that provides signals and slots.
     """