Версия 2 от 2010-05-24 14:25:54

Убрать это сообщение

Работа с базовыми составными блоками

Эта глава включает:

Даже простая программа на wxPython нуждается в применении стандартных элементов, например, таких как меню и диалогов. Все эти элементы выступают в роли составных блоков любого GUI-приложения, и вместе с такими виджетами как splash-окно (заставка), строка статуса (status bar) или диалог About, они обеспечивают более дружественную среду и придают Вашему приложению профессиональный вид и восприятие. В завершение первой части книги, мы обсудим с Вами создание программы, использующей все эти компоненты. Мы построим простую программу рисования, затем добавим эти блочные составные элементы и разъясним некоторые вопросы их использования. Мы закрепим фундаментальные понятия, затронутые в предшествующих главах, и в конечном счете у Вас получится простое, но в тоже время профессиональное приложение. Эта глава является промежуточным уровнем между базовыми понятиями, обсужденными в предшествующих главах и более подробным рассмотрением функциональности wxPython в частях 2 и 3.

Приложение, которое мы создадим в этой главе основано на примерах Doodle и Super Doodle, поставляемых вместе с wxPython в директории wxPython/samples. Это очень простая программа рисования, которая отслеживает указатель мыши и рисует линию при нажатии левой кнопки мыши. Рисунок 6.1 показывает простое исходное окно этой программы.

Рисунок 6.1 Образец окна без украшений

Мы выбрали такой пример, поскольку эта довольно простая программа иллюстрирует многие вопросы, возникающие при создании более сложных приложений. В этой главе мы покажем Вам, как нарисовать на экране линии, добавить панель состояния (status bar), панель инструментов (toolbar) и меню (menubar). Вы увидите, как можно использовать стандартные диалоги, такие как диалоги выбора файлов и цвета. Для размещения составных наборов виджетов мы используем координатор (sizer), а также добавим панель About и splash-окно. В конце главы Вы получите великолепную на вид заготовку программы.

Рисование на экране

Первоочередная задача Вашей программы для зарисовок состоит в рисовании на экране линии. Подобно другим GUI-инструментам, wxPython обеспечивает независимый от устройства набор инструментальных средств для рисования на различных типах дисплеев. В следующем разделе мы обсудим, как рисовать на экране.

Как рисовать на экране?

Для рисования на экране мы используем объект wxPython, называющийся контекстом устройства. Контекст устройства абстрагирует дисплейное устройство, предоставляя каждому устройству общий набор методов рисования, поэтому Ваш код рисования не зависит от типа целевого устройства. Контекст устройства в wxPython представлен абстрактным классом wx.DC и его подклассами. Так как wx.DC абстрактный класс, в своём приложении Вам нужно использовать один из его подклассов.

Использование контекста устройства

Таблица 6.1 отображает справочник по подклассам wx.DC и их использованию. Контексты устройств, которые используются для рисования в виджетах wxPython, должны всегда создаваться локально в виде временных объектов и не должны храниться между вызовами метода в виде глобальной переменной или другим способом. На некоторых платформах контексты устройства - это ограниченный ресурс и такое удержание ссылки на wx.DC может сделать программу нестабильной. Из-за способа, которым wxPython непосредственно использует контексты устройства, подклассы wx.DC имеют несколько тонких различий, которые учитываются при рисовании в виджетах. В главе 12 эти различия объясняются более подробно.

Таблица 6.1 Краткое руководство по подклассам контекста устройства wx.DC

Контект устройства

Использование

wx.BufferedDC

Используется для буферизации ряда команд рисования, до их завершения и готовности к дальнейшему рисованию на экране. Это предотвращает нежелательное мерцание при отображении.

wx.BufferedPaintDC

Тоже, что и wx.BufferedDC, но используется только в пределах wx.PaintEvent. Создавайте только временные экземпляры этого класса.

wx.ClientDC

Используется для рисования в оконном объекте. Применяйте этот класс, когда Вам необходимо рисовать в основной области виджета, т.е. не на границе или любых элементах оформления. Основная область иногда называется клиентской областью, отсюда и имя этого DC. Класс wx.ClientDC должен создаваться только временно. Этот класс используется только вне тела wx.PaintEvent.

wx.MemoryDC

Используется для рисования графики на сохраненном в памяти битовом изображении (bitmap), без его отображения. Вы можете затем выбрать битовое изображение и использовать метод wx.DC.Blit(), чтобы нарисовать его в окне.

wx.MetafileDC

В операционных системах Windows этот контекст устройства позволяет Вам создавать стандартные метафайлы данных.

wx.PaintDC

Идентичен контексту wx.ClientDC, за исключением того, что он используется только в пределах wx.PaintEvent. Создавайте только временные экземпляры этого класса.

wx.PostScriptDC

Используется для записи инкапсулированных файлов PostScript.

wx.PrinterDC

Используется в операционных системах Windows для вывода на принтер.

wx.ScreenDC

Используется для рисования непосредственно на экране, поверх и вне любого отображаемого окна. Этот класс должен создаваться только временно.

wx.WindowDC

Используется для рисования на всей области оконного объекта, включая границу и любые другие элементы оформления, не относящиеся к клиентской области. Операционные системы отличные от Windows могут не поддерживать этот класс.

Листинг 6.1 содержит начальный код изображенного на рисунке 6.1 окна рисования. Поскольку этот код показывает приёмы рисования в контексте устройства, мы его подробно прокомментируем.

Листинг 6.1 Начальный код окна рисования

   1 import wx
   2 class SketchWindow(wx.Window):
   3     def __init__(self, parent, ID):
   4         wx.Window.__init__(self, parent, ID)
   5         self.SetBackgroundColour("White")
   6         self.color = "Black"                                               
   7         self.thickness = 1
   8         self.pen = wx.Pen(self.color, self.thickness, wx.SOLID) # (1) Создание объекта wx.Pen
   9         self.lines = []
  10         self.curLine = []
  11         self.pos = (0, 0)                                  
  12         self.InitBuffer()                                   
  13         self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)            # (2) Присоединение событий
  14         self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
  15         self.Bind(wx.EVT_MOTION, self.OnMotion)
  16         self.Bind(wx.EVT_SIZE, self.OnSize)
  17         self.Bind(wx.EVT_IDLE, self.OnIdle)
  18         self.Bind(wx.EVT_PAINT, self.OnPaint)
  19                                      
  20     def InitBuffer(self):                                       # (3) Создание контекста устройства с буферизацией 
  21         size = self.GetClientSize()
  22         self.buffer = wx.EmptyBitmap(size.width, size.height)
  23         dc = wx.BufferedDC(None, self.buffer)
  24         dc.SetBackground(wx.Brush(self.GetBackgroundColour()))  # (4) Использование контекста устройства
  25         dc.Clear()
  26         self.DrawLines(dc)
  27         self.reInitBuffer = False                                  
  28 
  29     def GetLinesData(self):
  30         return self.lines[:]
  31     def SetLinesData(self, lines):
  32         self.lines = lines[:]
  33         self.InitBuffer()
  34         self.Refresh()
  35     def OnLeftDown(self, event):
  36         self.curLine = []
  37         self.pos = event.GetPositionTuple()                     # (5) Получение позиции мыши
  38         self.CaptureMouse()                                     # (6) Захват мыши
  39     def OnLeftUp(self, event):
  40         if self.HasCapture():
  41             self.lines.append((self.color,
  42                 self.thickness,
  43                 self.curLine))
  44             self.curLine = []
  45             self.ReleaseMouse()                                 # (7) Освобождение мыши
  46                                                             
  47     def OnMotion(self, event):
  48                                                             
  49         if event.Dragging() and event.LeftIsDown():             # (8) Определение продолжения операции перетаскивания
  50             dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)  # (9) Создание другого контекста с буферизацией
  51             self.drawMotion(dc, event)
  52         event.Skip()                                   
  53                                                       
  54     def drawMotion(self, dc, event):                            # (10) Рисование в контексте устройства 
  55         dc.SetPen(self.pen)
  56         newPos = event.GetPositionTuple()    
  57         coords = self.pos + newPos              
  58         self.curLine.append(coords)                
  59         dc.DrawLine(*coords)
  60         self.pos = newPos
  61 
  62     def OnSize(self, event):                                   # (11) Обработка события изменения размера
  63         self.reInitBuffer = True
  64  
  65     def OnIdle(self, event):                                   # (12) Обработка простоя
  66         if self.reInitBuffer:
  67             self.InitBuffer()
  68             self.Refresh(False)
  69 
  70     def OnPaint(self, event):                                  # (13) Обработка запроса на прорисовку
  71         dc = wx.BufferedPaintDC(self, self.buffer)
  72 
  73     def DrawLines(self, dc):                                   # (14) Рисование всех линий
  74         for colour, thickness, line in self.lines:
  75             pen = wx.Pen(colour, thickness, wx.SOLID)
  76             dc.SetPen(pen)           
  77             for coords in line:
  78                 dc.DrawLine(*coords)
  79 
  80     def SetColor(self, color):
  81         self.color = color
  82         self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
  83 
  84     def SetThickness(self, num):
  85         self.thickness = num
  86         self.pen = wx.Pen(self.color, self.thickness, wx.SOLID)
  87 
  88 class SketchFrame(wx.Frame):
  89     def __init__(self, parent):
  90         wx.Frame.__init__(self, parent, -1, "Sketch Frame",
  91             size=(800,600))
  92         self.sketch = SketchWindow(self, -1)
  93 
  94 if __name__ == '__main__':
  95     app = wx.PySimpleApp()
  96     frame = SketchFrame(None)
  97     frame.Show(True)
  98     app.MainLoop()

(1) Экземпляр wx.Pen определяет цвет, толщину и стиль нарисованных в контексте устройства линий. Стили отличные от wx.SOLID включают wx.DOT, wx.LONGDASH и wx.SHORTDASH.

(2) Для рисования этому окну необходимо отвечать на различные типы событий мыши. Оно реагирует на нажатие и отпускание левой кнопки мыши, движение мыши, изменение размеров окна и перерисовку окна. Также определяется обработка, которая происходит в течение простоя.

(3) Контекст устройства с буферизацией создается в два этапа: (1) Создается пустое битовое изображение, которое служит в качестве внеэкранного буфера и (2) На основе внеэкранного буфера создается буферизованный контекст устройства. Буферизованный контекст используется для того, чтобы избежать перерисовки проведенных линий рисунка и исключить вызываемое этим мерцание экрана. Позже в этом подразделе мы подробнее обсудим контексты устройства с буферизацией.

(4)' Эти строки инициируют команды рисования в контексте устройства, а именно – устанавливается кисть заполнения фона и очищается устройство. Объект wx.Brush определяет цвет и стиль фона для команд заливки.

(5) Метод GetPositionTuple() объекта event возвращает кортеж Python, содержащий точную позицию, в которой выполнен щелчок мыши.

(6) Метод CaptureMouse() направляет весь ввод мыши в данное окно, даже если Вы перемещаете мышь за пределами границы окна. Этот вызов должен отменяться последующим вызовом в программе метода ReleaseMouse().

(7) Вызов ReleaseMouse() возвращает систему в состояние предшествующее вызову CaptureMouse(). Для отслеживания окон, которые захватили мышь, приложение wxPython использует стек, а вызов ReleaseMouse() эквивалентен высвобождению этого стека. Это подразумевает, что Вам нужно равное количество вызовов CaptureMouse() и ReleaseMouse().

(8) Эта строка определяет, является ли событие перемещения мыши частью процесса проведения линии, характеризующегося происходящим событием движения мыши при ее нажатой левой кнопке. Методы Dragging() и LeftIsDown() являются методами wx.MouseEvent и возвращают True, если во время перемещения данное условие выполняется.

(9) Поскольку wx.BufferedDC – один из временно создающихся контекстов устройства, перед тем как мы нарисуем линии, нам нужно создать другой контекст. В данном случае, в качестве основного контекста устройства мы создаем новый wx.ClientDC и снова используем в качестве буфера наш экземпляр переменной битового изображения.

(10) Эти строки фактически используют контекст устройства для рисования на экране только что проведенной линии. Сначала, мы создаем кортеж координат coords, который является комбинацией кортежей self.pos и newPos. Здесь, новая точка поступает из события GetPositionTuple(), а старая точка была определена при последнем вызове OnMotion(). Мы сохраняем этот кортеж в списке self.curLine, а затем используем функциональный вызов с синтаксисом распаковки (unpack syntax) для вызова DrawLine() с элементами кортежа в качестве аргументов. Метод DrawLine() принимает в качестве параметров (x1, y1, x2, y2) и рисует линию из точки (x1, y1) в точку (x2, y2). Частота, с которой происходит событие перемещения и появляется новая точка линии, зависит от базовой скорости системы.

(11) Если размеры окна изменились, мы отмечаем это, сохраняя величину True в атрибуте self.reInitBuffer. Фактически же, мы ничего не делаем до следующего события простоя (idle event).

(12) Когда придет событие простоя, приложение получает возможность реагировать на одно или несколько имевших место событий изменения размеров. Причина, по которой мы реагируем на событие простоя, а не на собственно событие изменения размеров, состоит в том, чтобы разрешить многократным событиям изменения размеров проследовать друг за другом максимально быстро без необходимости каждый раз выполнять перерисовку.

(13) Обработка запроса на перерисовку изображения удивительно проста: для рисунка создается буферизованный контекст устройства. Создается реальный wx.PaintDC (так как мы находимся в обработчике прорисовки, нам нужен именно wx.PaintDC, а не экземпляр wx.ClientDC), и затем, после удаления экземпляра dc, битовое изображение блиттируется (копируется) в него. Более подробная информация о буферизации приведена в следующих разделах.

(14) Этот метод используется, когда в результате изменения размеров (и последующей загрузки из файла) приложению необходимо перерисовать линии из экземпляра данных. Кроме того, мы используем надстройку DrawFunc(). В этом случае, мы проходим список линий, хранящийся в атрибуте coords, повторно создаем перо для каждой линии (скоро будет добавлена поддержка изменения характеристик пера), а затем рисуем все принадлежащие этой линии координатные кортежи.

Использование буфера рисования в этот пример обеспечивается двумя специальными подклассами wx.DC. Буфер рисования – это неотображаемая область, где все Ваши базовые команды рисования выполняются поочередно, а затем копируются на экран за один шаг. Преимущество буфера - в том, что пользователь не замечает отдельно происходящих команд рисования, и таким образом, экран обновляется с меньшим мерцанием. По этой причине, буферизация обычно используется при анимации или когда чертеж состоит из множества мелких частей.

В wxPython имеется два класса, которые используются для буферизации: обычно используется для буферизации wx.ClientDC, а wx.BufferDC wx.BufferPaintDC используется для буферизации wx.PaintDC. Работают они практически одинаково. Буферизованный контекст устройства создается двумя аргументами. Первый аргумент – это контекст целевого устройства соответствующего типа (например, в строке 9 листинга 6.1, это новый экземпляр wx.ClientDC). Второй аргумент – это объект wx.Bitmap. В листинге 6.1, мы создаем битовое изображение при помощи функции wx.EmptyBitmap. Когда команды рисования относятся к буферизованному контексту устройства, для рисования битового изображения используется встроенный контекст wx.MemoryDC. При уничтожении буферного объекта, деструктор C++ использует метод Blit(), чтобы автоматически скопировать битовое изображение на целевое устройство. В wxPython уничтожение обычно происходит, когда объект покидает область действия. Поэтому буферизованные контексты устройства полезны только при их временном создании, при котором они уничтожаются, и выполняется блитирование (blit).

Например, в методе OnPaint() листинга 6.1, битовое изображение self.buffer уже определено. Буферный объект необходимо просто создать, устанавливая этим самым связь между существующим битовым изображением и временным контекстом wx.PaintDC() окна. Метод завершается, буферизованный DC немедленно покидает область, запускается деструктор и битовое изображение копируется на экран.

Функции контекста устройства

При использовании контекстов устройства не забывайте использовать корректный контекст в зависимости от вида выполняемого Вами рисования (особо помните различие между wx.PaintDC и wx.ClientDC). Только в случае, когда у Вас будет корректный контекст устройства, Вы сможете что-либо с ним делать. Таблица 6.2 перечисляет некоторые наиболее интересные методы wx.DC.

Таблица 6.2 Методы общего назначения wx.DC

Перевод: Савицкий Юрий