среда, 17 октября 2012 г.

wxPython in Action. Глава 12. Манипуляции с изображениями.

В этой главе Вы узнаете как:

  • Загружать изображения и создавать их
  • Создавать устройства контекста (device context)
  • Рисовать в устройствах контекста
  • Писать текст в устройствах контекста
  • Работать с карандашом, кистью и координатами устройств контекста
Самое основное действие, которое происходит при роботе с любым GUI - это рисование на экране. По большому счёту каждый виджет, определённый в wxPython, состоит из серии команд рисования, посылаемых на экран. Но эти команды зависят от того, является ли этот виджет "родным" для ОС, или полностью определяемым в wxPython. В этой главе мы поговорим о том, как контролировать wxPython на уровне базовых команд рисования. Кроме того, мы обсудим управление и отображение других графических элементов, таких как картинки и шрифты.
Первичная абстракция, которая используется в wxPython - это контекст устройства. Он использует стандартный API для рисования на устройствах типа монитора или принтера. В классах контекста устройств сосредоточена основная функциональность, необходимая для рисования, такая как рисование линий, кривых или текста.

12.1 Работа с изображениями

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

12.1.1 Как загрузить изображение

Работа с изображениями в wxPython имеет две стороны: платформонезависимые изображения обрабатываются классом wx.Image, a платформо-зависимые - классом wx.Bitmap. На практике это означает, что внешние форматы файлов загружаются и сохраняются при помощи wx.Image, тогда как wx.Bitmap занимается отображением картинки на экране. Рисунок 12.1 показывает создание различных изображений, считываемых из файлов различного формата.
Для загрузки изображения из файла используется конструктор wx.Image:
wx.Image(name, type=wx.BITMAP_TYPE_ANY, index=-1)
Рисунок 12.1 Большие и маленькие изображения разных типов

Параметр mane - это имя файла, откуда надо загрузить изображение, а type - обработчик типа изображения. Им может быть либо wx.BITMAP_TYPE_ANY, либо один из флагов, перечисленных в таблице 12.1. Если Вы используете wx.BITMAP_TYPE_ANY, тогда wxPython попробует самостоятельно определить тип файла. Если же Вы указываете конкретный тип - wxPython конвертирует файл с использованием этого типа. Листинг 12.1 показывает как можно загрузить изображение при помощи wx.BITMAP_TYPE_ANY:
Листинг 12.1
import wx 
filenames = ["image.bmp", "image.gif", "image.jpg", "image.png" ] 
class TestFrame(wx.Frame): 
    def __init__(self): 
        wx.Frame.__init__(self, None, title="Loading Images") 
        p = wx.Panel(self) 
        
        fgs = wx.FlexGridSizer(cols=2, hgap=10, vgap=10) 
        for name in filenames: 
            # загружаем изображения из файла0
            img1 = wx.Image(name, wx.BITMAP_TYPE_ANY) 


            

            w = img1.GetWidth() 
            h = img1.GetHeight() 
            # масштабируем изображения
            img2 = img1.Scale(w/2, h/2) 
            
            # делаем из изображений bitmap
            sb1 = wx.StaticBitmap(p, -1, wx.BitmapFromImage(img1)) 
            sb2 = wx.StaticBitmap(p, -1, wx.BitmapFromImage(img2)) 
            
            fgs.Add(sb1) 
            fgs.Add(sb2) 
            
        p.SetSizerAndFit(fgs) 
        self.Fit() 


app = wx.PySimpleApp() 

frm = TestFrame() 
frm.Show() 
app.MainLoop() 

Этот листинг должен быть вполне понятен сам по себе. Мы создаём список имён файлов с изображениями, которые мы хотим использовать. Проходя через него в цикле, мы сперва создаём экземпляр wx.Image c флагом типа any для того, чтобы wxPython сам определил формат файла. Затем мы уменьшаем изображение в два раза при помощи метода самого изображения и конвертируем его в bitmap, чтобы иметь возможность показать его на экране.
Хотя это пример поручает определение типа изображения wxPython'у, тем не менее Вы можете задать формат явно. В следующем разделе мы посмотрим, какие форматы изображений поддерживаются wxPython.

Поддерживаемые форматы изображений

Изображения управляются при помощи обработчиков изображений (image handlers). Обработчик изображения - это экземпляр wx.ImageHandler, предоставляющего расширяемую архитектуру для работы с различными форматами картинок. В нормальных условиях Вам вообще не нужно думать о том, как он работает. Всё, что Вам нужно знать - что каждый обработчик имеет собственный уникальный идентификатор wxPython для загрузки файла соответствующего формата. Поддерживаемые форматы перечислены в таблице 12.1. Флаг типа - это значение, используемое для определения типа изображения при его загрузке.
Таблица 12.1 Поддерживаемые форматы файлов wxPyon
Класс обработчкика Флаг типа Примечания
wx.ANIHandler wx.BITMAP_TYPE_ANY Анимированный формат курсора. Этот обработчик только загружает изображения, не сохраняет их
wx.BMPHandler wx.BITMAP_TYPE_BMP Формат bitmap для Windows и OS/2
wx.CURHandler wx.BITMAP_TYPE_CUR Формат курсора в Windows
wx.GIFHandler wx.BITMAP_TYPE_GIF Формат обмена графикой. Из-за ограничений прав собственности этот обработчик не сохраняет изображения
wx.ICOHandler wx.BITMAP_TYPE_ICO Формат иконок в Windows
wx.IFFHandler wx.BITMAP_TYPE_IFF Формат обмена файлов. Этот обработчик только загружает изображения, не сохраняет их
wx.JPEGHandler wx.BITMAP_TYPE_JPEG Формат Joint Photographic Experts Group
wx.PCXHandler wx.BITMAP_TYPE_PCX Формат PC Paintbrush. Когда сохраняются изображения в этом формате, wxPython подсчитывает число различных цветов в изображении. Если возможно, то изображение сохраняется как 8-и битное (то есть 256 цветов и меньше). Иначе, изображение сохраняется как 24-х битное.
wx.PNGHandler wx.BITMAP_TYPE_PNG Формат переносимой сетевой графики
wx.PNMHandler wx.BITMAP_TYPE_PNM Может только загружать ASCII или "сырые" RGB изображения. Изображения этим обработчиком сохраняются в формате RGB
wx.TIFFHandler wx.BITMAP_TYPE_TIF Формат теггированных изображений
wx.XPMHandler wx.BITMAP_TYPE_XPM Формат XPixMap
(auto) wx.BITMAP_TYPE_ANY Попробует автоматически определить формат и будет использовать соответствующий обработчик

Для того, чтобы использовать MIME для идентификации файла вместо ID типа используйте функцию wx.ImageFromMime(name, mimetype, index=-1), где name - имя файла, mimetype - строка с типом файла. Параметр index определяет какое изображение надо загрузить, если фаил содержит несколько изображений, что актуально для обработчиков GIF, ICO и TIFF. Значение по умолчанию (-1) означает, что надо выбрать то изображение, которое предоставляется по умолчанию, то есть первое изображение (index=0) для GIF и TIFF и самое большое и красочное изображение для ICO.

Создание объектов изображений

wxPython использует другие глобальные функции для создания различных видов wx.Image объектов. Для создания пустого изображения с определённым размером используется функция wx.EmptyImage(width, height) - все пиксели этого изображения будут чёрными. Для создания изображения из открытого потока или файлоподобного объекта Python используется wx.ImageFromStream(stream, type=wx.BITMAP_TYPE_ANY, index=-1). Иногда полезно создавать изображение из сырых данных RGB, что можно сделать при помощи wx.ImageFromData(width, height, data), где data - строка, в которой каждый набор трёх последовательных символов определяет красный, зелёный и синий компонент пикселя. Размер этой строки всегда должен быть равен width*height*3.

Создание объектов bitmap

Есть несколько способов создания объекта bitmap. Основной из них - конструктор wx.Bitmap - wx.Bitmap(name, type=wx.BITMAP_TYPE_ANY). Аргумент name - имя файла, type - может быть одним из типов, определённых в таблице 12.1. Если класс bitmap может сам обработать формат файла, то он так и сделает, иначе он загрузит его при помощи wx.Image и затем конвертирует его в экземпляр wx.Bitmap.
Вы так же можете создать пустой bitmap при помощи метода wx.EmptyBitmap(widht, height, depth=-1). Параметры width и height - размеры картинки, а depth - глубина цвета для будущего изображения. Есть две функции для создания bitmap из сырых данных. Функция wx.BitmapFromBits(bits, width, height, depth=-1) создаёт bitmap, где bits - это питоновский список байтов. Поведение этой функции зависит от платформы. В большинстве случаев биты могут принимать значения либо 0, либо 1, а сама функция создаёт монохромное изображение. Под Windows данные передаются непосредственно windows API функции CreateBitmap(). Функция wxBitmapFromXPMData(listOfStrings) принимает в качестве параметра список строк, которые рассматриваются в качестве данных в формате XPM.
Вы можете конвертировать изображения в bitmap и обратно c использованием конструктора wx.BitmapFromImage(image, depth=-1). Параметр image - это объект wx.Image, depth - глубина цвета результирующей bitmap. Если depth не определена, то используется текущая глубина изображения для экрана. Для конвертирования в обратную сторону используется функция wx.ImageFormBitmap(bitmap), которой передаётся объект wx.Bitmap. В листинге 12.1 создаётся объект bitmap ghb помощи соответствующего конструктора, а затем используется конструктор виджета wx.StaticBitmap, который позволяет разместить изображение в контейнере как любой другой элемент wxPython.

12.1.2 Что я могу сделать с изображением?

После того, как у Вас есть картинка, Вы можете проводить с ней множество разных полезных и интересных манипуляций.
Вы можете получить размер изображения при помощи методов GetWidth() и GetHeight(). Можно получить значение цвета по адресу пикселя при помощи GetRed(x, y), GetGreen(x, y) и GetBlue(x, y). "nb методы возвращают значения от 0 до 255 (в терминах С это unsigned int, однако это мало что значит в Python). Так же Вы можете задать цвет конкретного пикселя при помощи SetRGB(x, y, red, green, blue), где x и y - координаты пикселя, а red, green и blue - значения цвета в интервале между 0 и 255.
Вы так же можете получить всё данные рисунка одним огромным куском при помощи метода GetData(). Он возвращает большую строку, где каждый символ является членом RGB триплета и каждый символ является значением между 0 и 255. Значения расположены по порядку, где первое значение - значения для красного цвета для пикселя (0,0), второе - зелёного, а третье - синего для того же пикселя. Следующие три значения определяют цвет пикселя по адресу (0,1), и так далее. Всё это можно описать следующим псеводкодом на Python:

def GetData(self): 
    result = "" 
    for y in range(self.GetHeight()): 
        for x in range(self.GetWidth()): 
            result.append(chr(self.GetRed(x,y))) 
            result.append(chr(self.GetGreen(x,y))) 
            result.append(chr(self.GetBlue(x,y))) 
    return result 
Есть две вещи на которые надо обратить внимание при использовании соответствующего метода SetData(data), который использует строку с RGB значениями. Во-первых, метод SetData() не проверяет, попадают ли переданные значения в допустимый диапазон и соответствует ли их  количество размеру изображения. Если Вы где-то допустили ошибку - поведение может быть непредсказуемым. Во-вторых, из-за способа управления памятью кодом на C++, не стоит передавать методу SetData() значение, возвращаемое методом GetData() - надо создать промежуточную строку.
Строка с данными изображения может быть легко преобразована в или из любого другой Python тип данных, так что с RGB данными можно работать как с числами, массивом и т.д. Например, посмотрите на это:
import array
img=wx.EmptyImage(100,100)
a=array.array('B',img.GetData())
for i in range(len(a)):
    a[i] = (25+i) % 256
img SetData(a.tostring())
Таблица 12.2 показывает методы wx.Image, которые можно использовать для работы с изображениями. Эти методы являются стартовым набором для работы с изображениями. В следующем разделе я покажу два способа работать с более сложными темами - прозрачностью и полупрозрачностью.
Таблица 12.2 Методы работы с изображениями wx.Image
 
Метод Описание
ConvertToMono(r, g, b)
Возвращает wx.Image такого же размера, что и оригинал, где все пиксели с цветом (r, g, b) заменены белыми пикселями, а все остальные - чёрными. Оригинальное изображение остаётся без изменений
Mirror(horizontally = True) Возвращает зеркальное отображение исходного изображения. Если horizontally = True, тогда отображение происходит относительно горизонтальной оси, иначе - относительно вертикальной. Оригинальное изображение остаётся без изменений
Replace(r1, g1, b1, r2, g2, b2) Изменяет оригианльное изображение: каждый пиксель с цветом (r1, g1, b1) замещается на пиксель цвета (r2, g2, b2)
Rescale(width, height) Изменяет размер изображения на заданный размер.
Rotate(angle, rotationCentre, interpolating = True, offsetAfterRotation = None) Возвращает новое изображение, полученное поворотом оригинального изображения. Параметр angle - рациональное число, обозначающее угол поворота в радианах. rotationCentre - это wx.Point, вокруг которой происходит вращение. Если interpolating = True, то используется более точный, но и более медленный алгоритм. offsetAfterRotation - это точка, обозначающая, насколько должно быть сдвинуто изображение после поворота. Все пустые пиксели, незатронутые поворотом, будут окрашены в чёрный цвет или в цвет маски, если она есть у изображения
Rotate90(clockwise = True) Поворачивает изображение на 90 градусов в направлении, определяемым логическим параметром clockwise ("по часовой стрелке")
Scale(width, height) Возвращает копию изображения, отмасштабированное под новый размер

Установка маски изображения для определения прозрачности файла

Маска изображения - это специальный набор цветов в изображении, который отображается как прозрачные места изображения. Можно сказать, что это "зелёный экран", однако Вы можете использовать для этой цели любой цвет, не только зелёный. Вы можете задать маску изображения при помощи метода SetMaskColor(red, green, blue), где red, green и blue определяют цвет маски изображения. Если Вы хотите отключить маску - используйте SetMask(False), а для включения - SetMask(True). Метод HasMask() возвращает булево значение, установлена маска или нет. Вы так же можете задать маску из другого изображения такого же размера при помощи метода SetMaskFromImage(mask, mr, mg, mb) - в этом случае маска определяется для всех пикселей в маскирующем изображении с цветом mr, mg, mb, не зависимо от того, какой цвет у этих же пикселей в исходном изображении. Это обеспечивает дополнительную гибкость при создании маски, так как теперь Вам не нужно беспокоиться о цвете пикселей в исходном изображении. Получить цвет маски можно при помощи методов GetMaskRed(), GetMaskGreen() и  GetMaskBlue(). Если изображение с маской конвертировано в wx.Bitmap, то тогда маска автоматически преобразуется в объект wx.Mask и применяется к полученному изображению.

Установка альфа-значений для определения прозрачности файла

Альфа-значение - это ещё один способ определить прозрачность (или частичную прозрачность) изображения. Каждый пиксель имеет своё альфа-значение в интервале от 0 (если изображение в этой точке полностью прозрачно) и до 255 (если изображение в этой точке полностью непрозрачно). Задать эти значения можно при помощи метода SetAlphaData(data), который принимает строку байтов, аналогичную SetData(), но только с одним значением для пикселя. Так же как и SetData(), SetAlphaData() не проверяет значения на допустимые. Увидеть, установлены ли альфа-значения, можно при помощи HasAlpha() а получить их - при помощи GetAlphaData(). Можно так же задать значение для конкретного пикселя и получить его - SetAlpha(x,y,alpha) и GetAlpha(x,y).
В отличие от тех возможностей по работе с изображениями, которые Вам предоставляет wx.Image, wx.Bitmap может не так уж и много. Практически все методы wx.Bitmap - это получение значений таких свойств, как ширина, высота и глубина цвета.

12.1.3 Как я могу изменить курсор

Курсор - это "аватар" пользователя на экране. В этом качестве он служит для предоставления пользователю информации о том, в какой зоне он находится. Обычно курсор - это указатель на место, куда будет произведён клик пользователя, но в зависимости от виджета, на котором он находится, он моет принимать форму стрелки, текстового курсора I, или крестика. Если же приложение слишком занято, чтобы обработать клик пользователя, оно изменяет форму курсора на знак ожидания, например, на песочные часы.
Вы наверняка захотите использовать курсор для передачи сообщения о состоянии вашего приложения, и, хотя большинство виджетов wxPython сами изменяют тип курсора в зависимости от своего типа, тем не менее, Вам может захотеться сделать это самому. Может потому что Вы добавите свой виджет, а может потому что ваше приложение должно переопределить нормальное поведение курсора для сообщения о своём статусе. Возможно, Вы просто захотите, чтобы у пользователя в вашем приложении было своё изображение курсора. В этом разделе мы покажем как использовать курсор для сообщения о состоянии приложения и как менять его вид.
Вы можете создать объект курсора в wxPython разными способами. Класс курсора - wx.Cursor, и для создания его экземпляра есть два способа. Самый простой - метод wx.StockCursor(id), который возвращает экземпляр предопределённого в системе курсора. Как показано в таблице 12.3, параметр is может быть любым числом флагов.

Таблица 12.3 Предопределённые курсоры
Курсор ID Описание
wx.CURSOR_ARROW Обычный курсор
wx.CURSOR_ARROWWAIT Курсор занятости системы, когда показывается курсор и песочные часы. Доступно только для Windows
wx.CURSOR_BLANK Невидимый курсор. Когда хотите пошутить над пользователем
wx.CURSOR_BULLSEYE Курсор "бычий глаз" (кружок в кружке, полезно для точного позиционирования - http://en.wikipedia.org/wiki/%E2%97%89)
wx.CURSOR_CHAR Символьный курсор. Доступно не на всех платформах
wx.CURSOR_CROSS Вечно популярный курсор "прицел" - http://en.wikipedia.org/wiki/Reticle
wx.CURSOR_HAND Класический курсор в виде руки
wx.CURSOR_IBEAM Вертикальная черта, обычно используемая для указания места ввода текста в текстовом поле - |
wx.CURSOR_LEFT_BUTTON Курсор, изображающий мышь с нажатой левой кнопкой - используется для указания пользователю что надо нажать левую кнопку мыши. Может быть доступен не на всех платформах
wx.CURSOR_MAGNIFIER Увеличительное стекло, обчно указывает на увеличение области
wx.CURSOR_MIDDLE_BUTTON Курсор, изображающий мышь с нажатой средней кнопкой
wx.CURSOR_NO_ENTRY Курсор со стрейлкой и перечёркнутым кругом. Используется для обозначения того, что эта область на экране не доступна (например для drug and drop)
wx.CURSOR_PAINT_BRUSH Курсор, выглядящий как кисточка. Обычно используется в программах для работы с графикой
wx.CURSOR_PENCIL Курсор, выглядящий как карандаш. Обычно используется в программах для работы с графикой
wx.CURSOR_POINT_LEFT Курсор со стрелкой, указывающей влево
wx.CURSOR_POINT_RIGHT Курсор со стрелкой, указывающей вправо
wx.CURSOR_QUESTION_ARROW Курсор со расположенным рядом знаком вопроса, обычно используется для обозначения контекстной помощи
wx.CURSOR_RIGHT_ARROW Стандартный курсор, но отражённый в зеркале, так что он указывает нарпаво
wx.CURSOR_RIGHT_BUTTON Курсор, изображающий мышь с нажатой правой кнопкой
wx.CURSOR_SIZESW Один из курсоров, который указывает на два направления изменения размера. В этом случае указывает на нижний левый угол и правый верхний
wx.CURSOR_SIZENS Курсор изменения размера, указывающий вверх и вниз
wx.CURSOR_SIZENWSE Курсор изменения размера, указывающий в правый нижний угол и левый верхний
wx.CURSOR_SIZEWE Курсор изменения размера, указывающий налево и направо
wx.CURSOR_SIZING Курсор изменения размера, указывающий во все четыре стороны
wx.CURSOR_SPRAYCAN Ещё один курсор для рисования
wx.CURSOR_WAIT Курсор ожидания в виде песочных часов
wx.CURSOR_WATCH Курсор ожидания в виде часов

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

Создаём собственный курсор

Вы можете создать собственный курсор из изображения при помощи метода wx.CursorFromImage(image). Параметр image - экземпляр класса wx.Image, он автоматически приводится к размеру 32х32 (в MacOS - 16x16). Цвет маски изображения становится маской прозрачности курсора. По умолчанию, курсор указывает на угол изображения (0,0). Для того, чтобы это изменить, Вам надо задать это значение в самом изображении ещё до того, как использовать его в качестве курсора:
image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, 0)
image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, 22)
Сам же конструктор wx.Cursor таков:
wx.Cursor(name, type, hotSpotX=0, hotSpotY=0)
Параметр name - имя файла для загрузки картинки курсора. Параметр type - тип иконки для загрузки, он использует те же флаги wx.BITMAP_TYPE_, что и wx.Image и wx.Bitmap. Параметры  hotSpotX и hotSpotY определяют точку, куда указывает иконка. Если используется тип wx.BITMAP_TYPE_CUR для загрузки .cur файла, тогда точка указания автоматически устанавливается на определённую в .cur файле.
После того, как Вы создали курсор, можно использовать метод SetCursor(cursor) для каждого wxPython виджета. Этот метод изменяет форму курсора при его нахождении над этим виджетом. Кроме того, есть глобальный метод wx.SetCursor(cursor), который определяет форму курсора для всего приложения.
Картинки - это, конечно, хорошо, но это лишь малая часть того, что позволяет Вам сделать с графикой wxPython. В следующем разделе мы посмотрим на то, как посылать команды рисования на экран, файл или на другой отображаемый объект.

12.2 Работаем с контекстом устройств

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

12.2.1 Что такое контекст устройства и как его создать

Есть десять подклассов wx.DC в wxPython, разбитых на три группы:
  • Контекст, используемый для рисования на экране
  • Контекст, используемый для рисования не на экране
  • Контекст, используемый для буфера контекста устройства пока оно не будет готово для отображения.

Контексты экрана

Первая группа содержит контексты устройств, предназначенных для рисования на экране. Вы можете решить, что тут хватит одного класса, но на самом деле в wxPython их четыре, в зависимости от того, где и когда именно Вы хотите рисовать:
  • wx.ClientDC
  • wx.PaintDC
  • wx.WindowDC
  • wx.ScreenDC
Контекст устройств экрана подразумевает, что он создаётся временно. То есть, Вы должны создать его тогда, когда он понадобится Вам для рисования и не препятствовать его нормальному удалению. Никогда не надо пытаться сохранить его в качестве переменной - это не безопасно и может привести к нестабильной работе программы.
Обычно, Вы будете рисовать на экране используя либо wx.ClientDC или wx.PaintDC. Выбор одного из них зависит от того, когда Вы решите заняться рисованием. Если Вы хотите что-то изобразить на экране при перехвате события EVT_PAINT, то Вам нужно использовать wx.PaintDC. Во всех остальных случаях Вы должны использовать wx.ClientDC. На самом деле, когда Вы связываете обработчик с событием EVT_PAINT, Вы должны создать объект wx.PaintDC в методе обработчика, даже если Вы не собираетесь его использовать (если Вы не создадите его, то платформа может решить, что событие не было полностью обработано и пошлёт ещё одно событие). Причина того, что событие рисования требует различных контекстов устройств в том, что экземпляр wx.PaintDC оптимизирован только для рисования в области окна, которое перерисовывается, чтобы эта процедура происходила быстрее.
Вы создаёте клиента или контекст устройства при помощи простого конструктора, чьим единственным аргументом является виджет, на котором Вы будете рисовать - wx.ClientDC(window) или wx.PaintDC(window). Когда Вы используете эти контексты, Вы можете рисовать только в пределах этого виджета. Это значит, что рисуя во фрейме, Вы не сможете рисовать на его границах, полосе заголовка и т.п.
Если Вам надо рисовать во всей области фрейма, включая границы и прочую его отделку, Вы должны использовать wx.WindowDC. Вы можете создать wx.WindowDC так же, как Вы создаёте wx.ClientDC - при помощи конструктора wx.WindowDC(window). Так же как и wx.ClientDC, Вы не должны создавать wx.WindowDC во время события рисования - рисование на границах не совместимо с оптимизацией рисования в контексте устройства.
Иногда Вы не захотите быть ограниченными рисованием только на одном окне, может быть Вам понадобится весь экран. В этом случае, Вы можете использовать wx.ScreenDC. Опять же, его не надо создавать в событии рисования. Его конструктор не принимает аргументов (так как Вам не нужно определять окно, в котором Вы хотите рисовать) - wx.ScreenDC(). После этого Вы сразу можете использовать его как любой другой контекст устройства. Изображения, которые Вы будете рисовать, будут показаны поверх всех окон на вашем экране.

Не-экранные контексты

Вторая группа устройств контекста используется для рисования на чём-то, что экраном не является. Это что-то типа мешка для всего, что может быть рассмотрено как эквивалент экрану:
  • wx.MemoryDC
  • wx.MetafileDC
  • wx.PostScriptDC
  • wx.PrinterDC
Первое из этих устройств - wx.MemoryDC, которое позволяет Вам рисовать изображения, сохраняемые в памяти и не отображаемые на экране. Вы создаёте  экземпляр при помощи конструктора, не принимающего аргументы, но перед его использованием Вы должны ассоциировать с ним картинку (bitmap). Это делается вызовом метода SelectObject(bitmap) с аргументом типа wx.Bitmap. После этого, Вы можете рисовать в этом устройстве, изменяя связанное с ним изображение. Когда Вы завершите рисование, Вы можете воспользоваться методом Blit() для отображения картинки на окне. Более детально мы обсудим это устройство в следующем разделе.
Создание метафайлов в MS Windows облегчается использованием контекста wx.MetafileDC (доступен только под Windows - на других системах эта операция сама по себе достаточно проста). Вы создаёте устройство контекста при помощи конструктора с именем файла: wx.MetafileDC(filename=""). Если filename не передано, то метафайл создаётся в памяти, но Вы при этом ничего не теряете, так как многого с метафайлами Вы сделать в wxPyton не можете. После создания команды рисования, посланные в устройство записываются в файл. Когда рисование завершено, Вы можете по желанию воспользоваться методом Close(), который никак не влияет на сам файл, но зато возвращает объект wx.Metafile. В текущей версии wxPython единственное, что Вы можете сделать с этим объектом - передать его в буфер обмена при помощи метода SetClipboard(width=0, height=0).
Похожую функциональность, широко используемую на разных платформах, предоставляет объект wx.PostScriptDC, который создаёт инкапсулированный PostScript файлы (.eps). Вы создаёте файл wx.PostScriptDC из объекта данных принтера - wx.PostScriptDC(printData), где printData - это объект типа wx.PrintData, который мы будем обсуждать в 17 главе. После создания, контекст устройства можно будет использовать как любой другой контекст устройства. Имя файла для сохранения задаётся в объекте wx.PrintData. Так что Вы можете сохранить .eps файл как в этом примере:
data = wx.PrintData()
data.SetFileName("/tmp/test.eps")
data.SetPaperId(wx.PAPER_LETTER)
dc = wx.PostScriptDC(data)
dc.StartDoc("")
dc.DrawCircle(300,300,100)
dc.EndDoc() # на этом этапе файл и сохраняется
На Windows Вы можете получить доступ к любому принтеру используя wx.PrinterDC. Этот класс так же требует объект данных - wx.PrinterDC(printData) - и о нём мы тоже поговорим в 17 главе.

Буферы устройств контекста

Третья группа устройств контекста позволяет Вам хранить устройства контекста в буфере, пока они не будут готовы к отображению на экране:

  • wx.BufferedDC
  • wx.BufferedPaintDC
Буферизация позволяет Вам сперва выполнить отдельные команды рисования в буфере, а затем отобразить результат их выполнения на экране. Это избавляет экран от мерцания при нескольких перерисовках. В итоге буферизация - общепринятая техника для работы с анимацией или другими технологиями, требующими интенсивного рисования на экране.
Есть два буфера устройств контекста в wxPython - wx.BufferedDС, который можно использовать для буферизации любого устройства контекста (хотя обычно он используется только для wx.ClientDC), и wx.BufferedPaintDC, который разработан специально для wx.PaintDC. Оба буфера работают схожим образом, как некая обёртка вокруг памяти, где хранится устройство контекста. Конструктор wx.BufferedDC принимает в качестве параметра устройство контекста и bitmap в качестве опционального параметра:
wx.BufferedDC(dc, buffer=None)
C другой стороны, wx.BufferedPaintDC принимает окно и опциональный параметр bitmap:
wx.BufferedPaintDC(window, buffer=None)
Аргумент dc - устройство контекста, куда Вы хотите поместить изображение, после того, как Вы его нарисуете. В wx.BufferedPaintDC аргумент window используется для создания wx.PaintDC. Аргумент buffer - это bitmap, который используется в качестве временного буфера. Если он не указан, то он создаётся при создании экземпляра соответствующего буфера автоматически. После того, как буферное устройство контекста создано, Вы можете использовать его как обычное устройство контекста. При этом на самом деле рисунок отображается не на само устройство контекста, а на буферную память. По завершению рисования Вам не надо делать ничего дополнительного для того, чтобы показать рисунок на реальном устройстве контекста. Это происходит автоматически. Когда сборщик мусора собирает буферное устройство (обычно по завершении метода, когда устройство покидает область видимости), функция destructor C++ вызывает метод Blit(), который отображает итоговый рисунок из буфера на нужное устройство контекста без дополнительных усилий с Вашей стороны.

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

Теперь, когда у Вас есть устройства контекста, Вы наверняка захотите нарисовать на нём что-нибудь. Одно из преимуществ концепции устройств контекста в том, что вашей программе не нужно знать, с каким типом устройства она взаимодействует - команды для рисования будут одни и те же.
В wxPython есть много способов рисования на устройствах конеткста. Их API определяет порядка 80 методов. В таблице 12.4 перечислена первая группа методов - позволяющие рисовать геометрические фигуры. Если не указано обратное, то все эти методы используют текущий карандаш для рисования линий и текущую кисть для закраски формы. Детальнее о карандашах и кистях мы поговорим позже в этой главе.
Таблица 12.4 Методы устройств контекста для рисования геометрических фигур
Метод Описание
CrossHair(x, y) Рисует перекрестие на всём устройстве контекста. Точка пересечения горизонтальной и вертикальной линии - (x, y)
DrawArc(x1, y1, x2, y2, xc, yc) Рисует дугу из точки (x1, y1) в точку (x2, y2). Центр дуги - (xс, yс). Дуга рисуется против часовой стрелки из первой точки во вторую. Для заливки дуги используется текущая кисть
DrawCheckMark(x, y, width, height) Рисует "галочку", которую бы поставили в прямоугольник с левым верхним углом в (x, y) и размерами (widht, height). Кисть используется для закраски фона
DrawCircle(x, y, radius) Рисует круг с центром в (x, y) с заданым радиусом
DrawEllipse(x, y, width, height) Рисует эллипс, вписаный в прямоугольник с левым верхним углом в (x, y) и размерами (widht, height).
DrawEllipseArc(x, y, width, height, start, end) Рисует дугу эллипса. Первые четыре параметра аналогичны параметрам метода DrawEllipse. Параметры start и end определяют начальный и конечный углы эллипса отностиельно оси x. Углы задаются в градусах (360 - полный эллипс).  Положительное значение означает движение против часовой стрелки. Если start == end, то эллипс будет нарисован целиком.
DrawLine(x1, y1, x2, y2) Рисует линию из точки (x1, y1) в (x2, y2). (По общепринятым графическим соглашениям, последняя точка сама не отображается)
DrawLines(points, xoffset = 0, yoffset = 0) Рисует набор линий. Параметр points - список экземпляров wx.Point (или кортежей из двух элементов, которые преобразуются в wx.Point). Линии рисуются из точки в точку до конца списка. Если используется отступ (offset), то он применяется к каждой точке, позволяя нарисовать фигуру в любой точке устройства контекста. Кисть НЕ используется для заливки фигуры
DrawPolygon(points, xoffset = 0, yoffset = 0, fillstyle = wx.ODDEVEN_RULE) Рисует набор линий, аналогично предыдущему методу, но кроме того рисует линию между последней и первой точкой и закрашивает полученную фигуру кистью
DrawPoint(x, y) Рисует указанную точку карандашом
DrawRectangle(x, y, width, height) (x, y) - верхняя левая точка прямоугольника, а (width, height) задают его размеры
DrawRoundedRectangle(x, y, width, height, radius = 20) Аналогичен предыдущему методу, но углы замещены четвертями круга. Параметр radius определяет кривизну. Если он положительный, то он определяет радиус круга в пикслеях; если отрицательный - то размер круга пропорционален меньшему из размеров прямоугольника и расчитывается по формуле radius * dimension
DrawSpline(points) Принимает список точек и рисует сплайн. Полученная фигура не заливается
FloodFill(x, y, color, style = wx.FLOOD_SURFACE) Заливает пространство цветом кисти. Алгоритм начинается с точки (x, y). Если style = wx.FLOOD_SURFACE, тогда закрашиваются все соприкасающиеся пиксели заданного цвета. Если style = wx.FLOOD_BORDER, тогда закрашиваются все пиксели, ограниченные рамкой заданного цвета
Для всех методов Draw, которые принимают только два параметра x и y, есть соответсвуюбщий метод Draw...Point, который принимает вместо этих двух аргументов экземпляр wx.Point; например, DrawCirclePoint(pt, radius). Если же метод принимает две пары аргументов - координаты и размеры (width и height) есть соответствующий метод, который принимает wx.Point и wx.Size с названием Draw...PointSize; например, DrawRectanglePointSize(pt, sz). Кроме того, у них есть версия Draw...Rect, которая принимает экземпляр wx.Rect, например, DrawRectangleRect(rect).
Вы можете получить размер устройства контекста при помощи метода GetSize(), который возвращает экземпляр wx.Size. Вы можете получит значение цвета конкретного пикселя при помощи метода GetPixel(x, y), который возвращает экземпляр wx.Color.
Картинка 12.2 показывает что мы нарисуем при помощи известных нам методов и двойной буферизации.
Рисунок 12.2

В листинге 12.2 приведён код ля получения этого изображения - сетчатой диаграммы, которая отображает значения в интервале от 0 до 100 в полярной системе координат, предназначенной для более лёгкого поиска выделяющихся значений. Вы может использовать этот тип графика для наблюдений за показателями выделения ресурсов, в таком случае даже беглый взгляд даст Вам представление, оптимальна ли ситуация (для заданных условий) или нет (полный расход ресурсов). В нашем примере график обновляется случайными данными. Это большой пример, демонстрирующий несколько вещей, с которыми Вы уже знакомы.
Листинг 12.2
import wx 
import math 
import random 


class RadarGraph(wx.Window): 

    def __init__(self, parent, title, labels): 
        wx.Window.__init__(self, parent) 
        self.title = title 
        self.labels = labels 
        self.data = [0.0] * len(labels) 
        self.titleFont = wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD) 
        self.labelFont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL) 
        
        self.InitBuffer() 
        
        self.Bind(wx.EVT_SIZE, self.OnSize) 
        self.Bind(wx.EVT_PAINT, self.OnPaint) 
    
    def OnSize(self, evt): 
        # Когда изменяется размер окна нам нужен новый буфер
        self.InitBuffer() 
        
    def OnPaint(self, evt): 
        # обновляем окно из буфера
        dc = wx.BufferedPaintDC(self, self.buffer) 
    
    def InitBuffer(self): 
        # создаём буфер
        w, h = self.GetClientSize() 
        self.buffer = wx.EmptyBitmap(w, h) 
        dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) 
        self.DrawGraph(dc) 
    
    def GetData(self): 
        return self.data 
    
    def SetData(self, newData): 
        assert len(newData) == len(self.data) 
        self.data = newData[:] 
        # обновляем при изменении данных
        dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) 
        self.DrawGraph(dc) 
    
    def PolarToCartesian(self, radius, angle, cx, cy): 
        x = radius * math.cos(math.radians(angle)) 
        y = radius * math.sin(math.radians(angle)) 
        return (cx+x, cy-y) 
    
    def DrawGraph(self, dc): 
        # рисуем график
        spacer = 10 
        scaledmax = 150.0 
        
        dc.SetBackground(wx.Brush(self.GetBackgroundColour())) 
        dc.Clear() 
        dw, dh = dc.GetSize() 
        dc.SetFont(self.titleFont) 
        tw, th = dc.GetTextExtent(self.title) 
        # рисуем заголовок
        dc.DrawText(self.title, (dw-tw)/2, spacer) 
        
        # находим центр
        th = th + 2*spacer 
        cx = dw/2 
        cy = (dh-th)/2 + th 
        
        # вычисляем масштаб
        mindim = min(cx, (dh-th)/2) 
        scale = mindim/scaledmax 
        
        # рисуем оси
        dc.SetPen(wx.Pen("black", 1)) 
        dc.SetBrush(wx.TRANSPARENT_BRUSH) 
        dc.DrawCircle(cx,cy, 25*scale) 
        dc.DrawCircle(cx,cy, 50*scale) 
        dc.DrawCircle(cx,cy, 75*scale) 
        dc.DrawCircle(cx,cy, 100*scale) 
        
        dc.SetPen(wx.Pen("black", 2)) 
        dc.DrawLine(cx-110*scale, cy, cx+110*scale, cy) 
        dc.DrawLine(cx, cy-110*scale, cx, cy+110*scale) 
        
        dc.SetFont(self.labelFont) 
        maxval = 0 
        angle = 0 
        polypoints = [] 
        for i, label in enumerate(self.labels): 
            val = self.data[i] 
            # переводим значения данных в точки полигона
            point = self.PolarToCartesian(val*scale, angle, cx, cy) 
            polypoints.append(point) 
            x, y = self.PolarToCartesian(125*scale, angle, cx,cy) 
            # рисуем подписи
            dc.DrawText(label, x, y) 
            if val > maxval: 
                maxval = val 
            angle = angle + 360/len(self.labels) 
        
        c = "forest green" 
        if maxval > 70: 
            c = "yellow" 
        if maxval > 95: 
            c = "red" 
        # задаём цвет кисти
        dc.SetBrush(wx.Brush(c)) 
        dc.SetPen(wx.Pen("navy", 3)) 
        # рисуем точки полигона
        dc.DrawPolygon(polypoints) 
    
class TestFrame(wx.Frame): 
    def __init__(self): 
        wx.Frame.__init__(self, None, title="Double Buffered Drawing", size=(480,480)) 
        self.plot = RadarGraph(self, "Sample 'Radar' Plot", ["A", "B", "C", "D", "E", "F", "G", "H"]) 
        # Задаём случайные начальные значения
        data = [] 
        for d in self.plot.GetData(): 
            data.append(random.randint(0, 75)) 
        self.plot.SetData(data) 
        
        # Создаём таймер для обновления значений
        self.Bind(wx.EVT_TIMER, self.OnTimeout) 
        self.timer = wx.Timer(self) 
        self.timer.Start(500) 
        
    def OnTimeout(self, evt): 
        # симулируем положительный или отрицательный рост данных
        data = [] 
        for d in self.plot.GetData(): 
            val = d + random.uniform(-5, 5) 
            if val < 0: 
                val = 0 
            if val > 110: 
                val = 110 
            data.append(val) 
        self.plot.SetData(data) 
        
app = wx.PySimpleApp() 
frm = TestFrame() 
frm.Show() 
app.MainLoop()

  1. Этот метод сам по себе не требует никаких команд рисования. Объект буфера автоматически перемещает self.buffer в wx.PaintDC когда устройство контекста разрушается в конце метода. Поэтому не требуется никакого дополнительного рисования - мы уже всё сделали.
  2. Мы создали буферное изображение того же размера, что и окно, после чего нарисовали в нём наш график. Так как мы используем wx.BufferedDC, всё, что мы нарисуем в буфере будет так же нарисовано в окне после того, как будет завершён метод InitBuffer.

12.2.3 Как отображать в контексте изображения?

Хотя и существуют объекты image и bitmap, упомянутые в начале этой главы, тем не менее Вам всё равно будут нужны методы устройств контекста для того, чтобы нарисовать изображение или скопировать его из одного устройства контекста в другое. Этот процесс называется blit (вспышка). Наиболее частое использование этой возможности - нарисовать часть изображения в контексте. Исторически это использовалось для того, чтобы позволить программе разместить все свои изображения в одном файле а затем рисовать нужные части этого изображения на конкретной кнопке.
Есть три метода устройств контекста для того, чтобы рисовать изображения в контексте из другого контекста или из уже существующего изображения:
  • Blit()
  • DrawBitmap()
  • DrawIcon()
Возможно, самый главный метод, и наиболее сложный, это Blit():
Blit (xdest, ydest, width, height, source, xsrc, ysrc, logicalFunc=wx.COPY, useMask=False, xsrcMask=-1, ysrcMask=-1)

Копируем часть изображения

Другое назначение Blit() - быстрое копирование пикселей из одного устройства контекста в другое. Обычно это используется когда Вы хотите скопировать часть одного изображения в другое изображение или быстро перенести пиксели на экран. Мы уже видели как Blit() используется для управления буферизованым устройством контекста. Это очень мощный и гибкий метод с набором параметров. В основном они включают в себя задание прямоугольной области в целевом устройстве контекста, в исходном изображении и несколько параметров самого копирования. Устройством назначения этого метода является экземпляр wx.DC, чей метод Blit() был вызван. Параметры xdest и ydest задают верхний угол прямоугольника, в который будут копироваться данные, в целевом устройстве. Параметры width и height задают размер прямоугольника, который будет скопирован. source - другой экземпляр wx.DC, откуда и происходит копирование. Параметры xsrc и ysrc определяют место в источнике изображения, откуда начётся копирование. Обычно они не совпадают с xdest и ydest. Параметры width и height одинаковы для устройств источника и назначения, так как оба прямоугольника должны быть одного размера.
logicalFunc - это алгоритм, который используется для слияния старых пикселей и новых. По умолчанию старые пиксели перезаписываются, однако могут быть определены и другие виды XOR поведения. В таблице 12.6 мы увидим полный список логических функций. Если параметр useMask установлен в true, blit выполняется с маской, определяющей, какие конкретно пиксели будут скопированы. В этом случае источником должно быть bitmap с ассоциированной маской или альфа-каналом. Если определены параметры xsrcMask и ysrcMask где начинается копирование в маске. Если они не определены, тогда используются параметры xsrc и ysrc. Есть так же версия метода BlitPointSize(), который заменяет все три пары точек экземплярами wx.Point, а ширину и высоту - wx.Size.

Рисование bitmap

Предположим, Вы хотите нарисовать целое изображение в вашем устройстве контекста. В таком случае у Вас есть набор простых методов, которые Вы можете использовать. Для того, чтобы нарисовать bitmap у Вас есть DrawBitmap(bitmap, x, y, useMask=False). Параметр bitmap - объект wx.Bitmap, который рисуется в устройстве контекста в точке (x, y). Булевый параметр useMask определяет как будет нарисовано изображение. Если он установлен в False, то просто рисуется изображение. В противном случае, если изображение содержит маску или ассоциированный альфа-канал, маска используется для того, чтобы определить, какая часть изображения будет прозрачной. Если изображение монохромно, то для него используются текущие цвета фона и текста, в противном случае будет использована собственная цветовая схема изображения. На рисунке 12.3 изображён возможный результат использования этого метода.
Рисунок 12.3 несколько раз нарисованное лицо

В листинге 12.3 приведён код, выводящий это изображение:

import wx 
import random 
random.seed() 

class RandomImagePlacementWindow(wx.Window): 
    def __init__(self, parent, image): 
        wx.Window.__init__(self, parent) 
        # получаем bitmap
        self.photo = image.ConvertToBitmap() 
        
        # вычисляем случайное место
        self.positions = [(10,10)] 
        for x in range(50): 
            x = random.randint(0, 1000) 
            y = random.randint(0, 1000) 
            self.positions.append( (x,y) ) 
            
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        
    def OnPaint(self, evt): 
        dc = wx.PaintDC(self) 
        brush = wx.Brush("sky blue") 
        dc.SetBackground(brush) 
        # заливаем устройство контекста кистью
        dc.Clear() 
        
        # рисуем bitmap
        for x,y in self.positions: 
            dc.DrawBitmap(self.photo, x, y, True) 
    
class TestFrame(wx.Frame): 
    def __init__(self): 
        wx.Frame.__init__(self, None, title="Loading Images", 
        size=(640,480)) 
        img = wx.Image("masked-portrait.png") 
        win = RandomImagePlacementWindow(self, img) 

app = wx.PySimpleApp() 
frm = TestFrame() 
frm.Show() 
app.MainLoop() 

Кроме того, Вы можете нарисовать wx.Icon при помощи метода DrawIcon(icon, x, y), который размещает объект wx.Icon в точке с координатами (x, y). Любое другое изображение, которое Вы хотите нарисовать должно быть сперва конвертировано либо в wx.Bitmap либо в wx.Icon.

12.2.4 Как я могу писать текст в устройство контекста

Для того, чтобы писать текст в устройство контекста можно использовать метод DrawText(text, x, y). Строка текста передаётся в параметре text, а параметры x и y определяют верхний левый угол текста. Для наклонного текста предназначен метод DrawRotatedText(text, x, y, angle). Параметр angle принимает значение в градусах. Положительное число поворачивает текст по часовой стрелке, а отрицательное - против. Так же доступны соответствующие методы DrawTextPoint(text, pt) и DrawRotatedTextPoint(text, pt, angle).
Стиль текста контролируется в самом устройстве контекста. Оно позволяет задать шрифт, цвет текста и фона. Для этого предназначены методы GetTextForeground(), SetTextForeground(color), GetTextBackground(), SetTextBackground(color), GetFont() и SetFont(font). В wxPython Вы можете определить должен ли текст иметь фоновый цвет при помощи метода SetBackgroundColor(mode). Значениями mode могут быть wx.SOLID, если Вы хотите чтобы у текста был фон, и wx.TRANSPARENT, если Вы хотите чтобы текст был на прозрачном фоне.
Когда мы размещаем текст на экране крайне полезно знать сколько места он там займёт. Если Вам достаточно просто знать размеры текущего шрифта, то используйте методы GetCharHeight()  и GetCharWidth(), которые возвращают среднюю высоту и ширину символа для текущего шрифта.
Кроме того, Вы можете определить точное место занимаемое заданной строкой при помощи метода GetTextExtent(string). Этот метод возвращает кортеж (width, height), определяющий размеры прямоугольника, описывающего этот текст, если бы он был изображён текущим шрифтом. Для получения более детальной информации используйте метод GetFullTextExtend(string), который возвращает кортеж (width, height, descent, externalLeading). descent определяет расстояние между номинальной базовой линией шрифта и нижней стороной прямоугольника, а externalLeading указывает на дополнительное пространство над шрифтом, которое обычно равно нулю. Другой полезный метод - GetPartialTextExtend(text), который возвращает список, каждый элемент которого равен ширине текста вплоть до соответствующего символа. То есть первый элемент показывает ширину первого симовола, второй - первых двух, третий - трёх, и т.д. Другими словами, он возвращает список, вычисляемый таким образом:
widths[i] = GetTextExtend(text[:i])[0]
Список может быть использован для определения количества символов, которые можно уместить на выбранном месте.
Все графические операции регулируются абстракциями карандаша и кисти. Об этом мы поговорим в следующей части.

12.3 Графические манипуляции

Работа с графикой не ограничивается изображениями и устройствами контекста. Есть и другие абстракции, которые позволяют управлять изображением, которое Вы создаёте. В этом разделе мы поговорим о карандаше, предназначенном для рисования линий, кисти для закрашивания фона, работе с пикселями и об именах цветов.

12.3.1 Как я могу управлять карандашом?

Цвет и стиль линий, которые Вы рисуете в устройстве контекста управляются текущим карандашом, который является экземпляром класса wx.Pen. Для того чтобы получить текущий карандаш используйте метод устройства контекста GetPen(). И, как не удивительно, для установки карандаша используется метод SetPen(pen).
Класс wx.Pen является простой структурой с несколькими свойствами, относящимся к рисованию линий. Конструктор класса обозначает некоторые эти свойства:
wx.Pen(colour, width=1, style=wx.SOLID)
Параметр colour либо является объектом wx.Colour, либо любым объектом, который может быть в wx.Colour автоматически конвертирован - кортеж RGB значений, строка с именем цвета или строка с 16иричными значениями RGB, например "#12C588". Параметр width определяет толщину линии карандаша в пикселях. Параметр style определяет способ отображения линии. В таблице 12.5 приводятся допустимые значения этого параметра (не все значения поддерживаются на всех платформах):
Таблица 12.5 Стили рисования wx.Pen
Стиль Описание
wx.BDIAGONAL_HATCH Карандаш будет рисовать обратную штриховку (\\\\)
wx.CROSSDIAG_HATCH Комбинация wx.BDIAGONAL_HATCH и wx.FDIAGONAL_HATCH - другими словами хххххх
wx.CROSS_HATCH Крестики ++++++
wx.DOT ........
wx.DOT_DASH ._._._._
wx.FDIAGONAL_HATCH //////////
wx.HORIZONTAL_HATCH короткая горизонатльная штриховка
wx.LONG_DASH — — — — — — 
wx.SHORT_DASH -------
wx.SOLID сплошная линия. поведение по умолчанию
wx.STIPPLE использует предоставленную картинку вместо острия карандаша
wx.TRANSPARENT не рисует ничего
wx.USER_DASH Использует заданный пользователем шаблон
wx.VERTICAL_HATCH вертикальная штриховка
В дополнение к конструктору есть ещё набор предопределённых карандашей, перечисленный ниже. Суть этих карандашей должна быть понятна из их названия, а все атрибуты не указанные в названии установлены в значения по умолчанию:
wx.BLACK_DASHED_PEN
wx.BLACK_PEN
wx.CYAN_PEN
wx.GREEN_PEN
wx.GREY_PEN
wx.LIGHT_GREY_PEN
wx.MEDIUM_GREY_PEN
wx.RED_PEN
wx.TRANSPARENT_PEN
wx.WHITE_PEN
Цвет, толщина и стиль крандаша доступны и полсе его создания при помощи методов GetColour(), SetColour(color), GetWidth(), SetWidth(width), GetStyle(), SetStyle(style). Кроме того есть ряд других свойств карандаша, которые Вы можете изменить. Одно из них - "end cap" - то, как карандаш обозначает конец линии. Обычно это заметно только для линий, чья толщина больше чем один пиксель. Для этого служат методы GetCap() и SetCap(cap). Доступные для установки значения - wx.CAP_BUTT, заканчивающий линию straight edge, wx.CAP_PROJECTING, помещающий на конец линии square projection, и wx.CAP_ROUND, заканчивающий линию полукругом. Значение по умолчанию - wx.CAP_ROUND. Кроме того, Вы можете определить то, как будет отображаться пересечение двух линий. Для этого используются методы wx.GetJoin() и wx.SetJoin(join). Доступные значения для установки - wx.JOIN_BEVEL, когда внешние углы двух линий соединяются прямыми линиями, wx.JOIN_MITER, где внешние углы продолжаются пока они не встретятся в какой-то точке, и, значение по умолчанию, wx.JOIN_ROUND, когда на место пересечения двух линий помещается кружок.
Если Вы создаёте карандаш со стилем wx.USER_DASH, то Вы можете задать стиль штриха и получить его при помощи методов GetDashes() и SetDashes(dashes). Параметр dashes - это список 16-и битных строк из 1 или нулей. Каждая единица будет интерпретироваться как точка в один пиксель, а каждый ноль - как пропуск в один пиксель. При рисовании линии карандаш будет по очереди использовать шаблон штриха полученный из каждой строки. Если у карандаша задан стиль wx.STIPPLE Вы можете установить bitmap, который будет использоваться как пунктир, при помощи метода SetStipple(stipple), а получить заданный пунктир при помощи метода GetStipple().
Когда Вы рисуете при помощи карандаша (или делаете блиттинг из другого изображения) то алгоритм, который используется для установки пикселя в месте назначения называется logical function и для его установки используется метод SetLogicalFunction(function). Значение по умолчанию - wx.COPY, которое просто копирует пиксель из источника в место назанчения как он есть. Другие логические функции могут применять разные биовые операции используя цвета источника и места назначения. Наиболее часто используемые операции - wx.XOR и wx.INVERT, обе они могут быть использованы для управления rubber-banding или другим графическим механизмом, где Вам нужно временно изменить цвет пикселя а затем вернуть его обратно. И это далеко не всё, что Вам предлагает wxPython. В таблице 12.6 приведён полный список доступных функций. В качестве обозначений логических операций используются стандартные символы битовых операций Python:
& битовое и
| битовое или
^ битовое исключающее или
~ битовое отрицание
Таблица 12.6 Логические функции
Функция Алгоритм
wx.AND source & destination
wx.AND_INVERT ~source & destination
wx.AND_REVERSE source & ~destination
wx.CLEAR Все пиксели устанавливаются в чёрный цвет (0)
wx.COPY source
wx.EQUIV ~source ^ destination
wx.INVERT ~destination
wx.NAND ~source | ~destination
wx.NOR ~source & ~destination
wx.NO_OP destination
wx.OR source | destination
wx.OR_INVERT ~source | destination
wx.OR_REVERSE source | ~destination
wx.SET Все пиксели устанавливаются в белый цвет (1)
wx.SRC_INVERT ~source
wx.XOR source ^ destination
Ещё раз напоминаем, что логические функции применяются каждый раз, когда пиксель рисуется в устройстве контекста, не важно, используя функцию рисования или блиттинг.

12.3.2 Как я могу управлять кистью для рисования фона?

Все операции заполнения, включая заливку или закрашивание элемента созданного при помощи функций рисования, управляются при помощи текущей кисти устройства контекста, которая является экземпляром класса wx.Brush. Вы можете получить текущую кисть при помощи GetBrush() и задать свою при помощи SetBrush(brush). Вызов метода устройства конекста Clear() закрашивает всё устройство используя текущую кисть.
Экземпляр кисти чуть проще чем карандаша. Вот его конструктор:
wx.Brush(colour, style=wx.SOLID)
Как понятно, colour определяет цвет кисти. К нему применимо всё, что мы говорили о цвете карандаша. Стиль кисти определяет то, как она закрашивает пространство. Доступны такие варианты:
wx.BDIAGONAL_HATCH
wx.CROSSDIAG_HATCH
wx.CROSS_HATCH
wx.FDIAGONAL_HATCH
wx.HORIZONTAL_HATCH
wx.SOLID
wx.STIPPLE
wx.TRANSPARENT
wx.VERTICAL_HATCH
Цвет и стиль могут быть изменены или получены при помощи GetColour(), SetColour(colour), GetStyle(), SetStyle(style). Единственная вещь, которую Вы ещё можете сделать с кистью - это установить шаблон пунктира при помощи SetStiple(bitmap). Если Вы создаёте пунктир для кисти, тогда надо использовать стиль wx.STIPPLE. Получить шаблон пунктира обратно можно при помощи GetStipple.

12.3.3 Как я могу манипулировать логическими и физическими координатами устройства

В этом разделе мы поговорим о различных способах которыми wxPython позволяет Вам работать с координатами, размерами и т.д. Давайте начнём с координат. Обычно система координат в wxPython для устройств конекста начинается в верхнем левом углу и растёт по направлению вправо-вниз. Это является стандартом для практически любой графической системы и, как понятно, ось y отличается от своего математического представления. Если же Вас это смущает, то изменить направление оси можно при помощи метода SetAxisOrientation(xLeftRight, yBottomUp); оба параметра логические. Если xLeftRight = True, то ось x растёт слева нарпаво и наоборот в противном случае. Точно так же если yBottomUp = True, то ость y растёт снизу вверх, и наоборот, если False.
Координаты измеряются в пикселях. Однако, иногда Вам может потребоваться использовать какие-то более реальные величины. На это случай Вы можете воспользоваться режимом масштабирования при помощи метода SetMapMode(mode). Благодаря этому режиму устройство контекста будет автоматически преобразоваывать логические координаты, указанные Вами в физические координаты экрана. Доступные режимы указаны в таблице 12.7
Таблица 12.7 режимы координат устройств контекста
Режим Логическая единица
wx.MM_LOMETRIC 0.1 мм
wx.MM_METRIC 1 мм
wx.MM_POINTS точка (как мера печати) - 1/72 дюйма
wx.MM_TEXT Значение по умолчанию - 1 пиксель
wx.MM_TWIPS Твип
Точность преобразования зависит от того, насколько хорошо ваша система работает с шагом точек (dot pitch) монитора, чтобы произвести необходимые преобразования. Вы можете посмотреть значение, используемое для преобразования при помощи метода GetPPI(), который возвращает количество пикселей, приходящихся на дюйм. На практике я не полагаюсь на то, что логичский дюйм соответствует физическому.
Режим масштабирования автоматически применяется когда Вы используете методы устройства контекста. Иногда же Вам надо провести преобразования вне устройства. Для того, чтобы перевести логические координаты в координаты устройства используется метод LogicalToDeviceX(x) и LogicalToDeviceY(y). Оба этих метода берут числовую величину обозначающую логическую координату, проводят преобразования и возвращают координату устройства. Есть связанные методы, которые используют режим масштабирования, но не обращают внимание на текущее направление осей, принимая абсолютное значение координат. Эти методы называются LogicalToDeviceXRel(x), LogicalToDeviceYRel(y).
Обратный набор методов, которые принимают локальные координаты устройства и переводят их в логические координаты называются, как можно догадаться, DeviceToLogicaX(x), DeviceToLogicaY(y), DeviceToLogicaXRel(x), DeviceToLogicaYRel(y) .
Есть набор методов устройств контекста, которые позволяют получить информацию и управлять частью устройства контекста в которое Вы рисуете. Зачастую Вы хотите ограничить область рисования в устройстве контекста. Обычно это делают для повышения производительности, в частности, если Вы знаете, что должна быть перерисована только часть изображения. Такая перерисовка называется клиппинг (clipping), а метод - SetClippingRegion(x, y, width, height). Четыре параметра определяют прямоугольник левым верхним углом и размерами. После того, как эта область установлена, все операции по рисованию будут применяться только к этой области. Для того, чтобы снять это выделение используйте метод DestroyClippingRegion(), снимает это выделение. После того, как этот метод вызван, все действия по рисованию затрагивают всё устройство контекста. Для получения координат заданного региона используйте метод GetClippingBox(), который возвращает кортеж (x, y, width, height).
Когда Вы рисуете в устройстве контекста, wxPython работает с минимальным прямоугольником, который описывает всё, что Вы нарисовали. Это прямоугольник называется ограничительная рамка (bounding box), и зачастую полезно определить не требуется ли обновить контекст. Вы можете получить все четыре стороны этой области при помощи методов MaxX(), MaxY(), MinX(), MinY(). Эти методы возвращают минимальные и максимальные значения координат для этой области. Если есть какая-то точка, которую Вы по какой-то причине хотите добавить в эту область - используйте метод CalcBoundingBox(x, y), который пересчитает размер области так, как если бы Вы нарисовали что-то через эту точку. Вы можете начать расчёт области заново при помощи метода ResetBoundingBox(). После этого, как Вы воспользуетесь этим методом, ограничительная рамка возвращается к значению по умолчанию, как если бы в устройстве контекста не было ничего нарисовано.

12.3.4 Предопределённые имена цветов

Следующие имена цветов гарантированно распознаются wxPython:
aquamarine black blue blue violet
brown cadet blue coral cornflower blue
cyan dark gray dark green dark olive green
dark orchid dark slate blue dark slate gray dark turquoise
dim gray firebrick forest green gold
goldenrod gray green green yellow
indian red khaki light blue light gray
light steel blue lime green magenta maroon
medium aquamarine medium blue mediu forest green medium goldenrod
medium orchid medium sea green medium slate blue medium spring green
medium turquoise medium violet red midnight blue navy
orange orange red orchid pale green
pink plum purple red
salmon sea green sienna sky blue
slate blue spring green steel blue tan
thistle turquoise violet violet red
wheat white yellow yellow green
Дополнительный набор имён цветов может быть загружен в память при помощи функции updateColourDB из модуля wx.lib.colourdb.

12.4 Итог

  • В wxPython Вы легко можете использовать стандартные графические операции, включая работу с изображениями и рисование на экране. Для работы с изображениями используется класс wx.Image, который поддерживает платформонезависимые инструменты, такие как загрузка изображений стандартных форматов, и класс wx.Bitmap, который предназначен для платформозависимых задач, таких как рисование изображения на экране. Предопределённые обработчики изображений есть для большинства распространённых форматов. После того, как Вы создали экземпляр wx.Image Вы можете применять к нему различные фильтры. Можно определить маску прозрачности, что сделает указанный цвет изображения прозрачным. Кроме того, можно задать набор альфа-значений, что позволяет определить прозрачность изображения в конкретных пикселях.
  • bitmap может быть создан из bitmap файла или конвертирован из изображения. Единственный смысл хранить изображение в классе wx.Bitmap - для вывода его на экран.
  • Вы может создать свой собственный курсор или использовать любой из дюжины уже готовых, включая наиболее часто используемые стрелки и курсоры ожидания. Курсор можно создать и из экземпляра wx.Image.
  • Рисование на экране или на любом виртуальном устройстве управляется через устройства контекста класса wx.DC, который является абстрактным классом, определяющим общее API для рисования. Различные подклассы wx.DC позволяют рисовать на экране, в памяти или в файл или в принтер. Устройства контекста должны быть созданы локально в программе и не должны храниться в глобальной области видимости. Когда Вы рисуете на экране, то тип устройства контекста зависит от того, рисуете Вы или нет в обработчике события EVT_PAINT. Есть так же отдельные устройства контекста, которые позволяют Вам рисовать за пределами обычной области окна или в произвольной области экрана, даже за пределами окна вообще.
  • Другие типы устройств контекста, такие как wx.MemoryDC или wx.BufferedDC, позволяют Вам рисовать напрямую в память для буферизации операций рисования, чтобы вывести на экран уже готовое изображение. Классы wx.BufferedDC и wx.BufferedPaintDC позволяют облегчить работу с буферными изображениями.
  • Некоторые методы позволяют Вам рисовать линии или геометрические фигуры в устройство контекста. Многие из них имеют вторую форму, позволяющую задать размеры или точку при помощи wx.Size или wx.Point, без того, чтобы разбивать фигуру на части. В устройство контекста так же можно написать текст, повернув не нужный угол. Вспомогательные методы дают Вам возможность определить шрифт и место на экране, которое займёт этот текст.
  • Кроме того, что Вы можете нарисовать bitmap в устройстве контекста, Вы так же можете использовать метод Blit(), который позволяет совершить быстрое копирование части изображения из одного устройства контекста в другое. Кроме того, в устройство контекста можно поместить иконку.
  • Вы можете управлять цветом и стилем вашего рисования при помощи экземпляра wx.Pen, определяющего стиль рисования, и wx.Brush, определяющего заливку фона. В обоих случаях устройства контекста работают с текущим значением объекта, которое Вы можете изменить. Вы можете задать цвет и стиль для обоих объектов, а для карандаша Вы так же можете определить ширину и шаблон штриха. Взаимодействие пикселя, нарисованного карандашом с уже имеющимся пикселем определяется логическими функциями. По умолчанию просто копируется цвет из источника в место назначения  но Вы можете задать и своё поведение.
  • Устройства контекста рисуют в физических координатах - пикселях, но Вы можете определить и параллельную систему координат в дюймах или миллиметрах и переводить координаты из одной системы в другую в процессе рисования. Кроме того, Вы можете задать область, которая ограничит доступное для рисования место. С другой стороны есть ограничительная рамка, которая определяет место, где Вы уже рисовали.

Комментариев нет:

Отправить комментарий