Еще одна интересная статья от Андрея Коновалова. В статье рассмотрены особенности реализации отрисовки изображений с прозрачностью при использовании .NET Compact Framework.

Вступление

К большому сожалению разработчиков, Compact Framework, да и native-функции тоже, не поддерживают альфа-канал с разной прозрачностью у индивидуальных пикселей. Это означает, что нет возможности создавать красоту неописуемую с плавными переходами между изображениями. Однако, что же делать, если хочется иметь хотя бы подобие “полного” альфа-канала, а именно, выводить полупрозрачные изображения, у которых есть ещё и полностью прозрачные участки?

Рассмотрим два способа вывода изображений с прозрачностью.

Способ №1. Фиксированый цвет является прозрачным

public static void DrawImageTransparent(Graphics g, 
  Bitmap b, Point location, Color transColor)
{
  if (b == null || g == null)
    return;

  ImageAttributes attrib = new ImageAttributes();
  attrib.SetColorKey(transColor, transColor);

  Rectangle destRect = new Rectangle(location.X, location.Y, b.Width, b.Height);
 
  g.DrawImage(b, destRect, 0, 0, b.Width, b.Height, GraphicsUnit.Pixel, attrib);
}

Стоит заметить, что только эта хитрая разновидность DrawImage позволяет выводить изображение с указанным ColorKey, по которому определяется, какие пиксели не рисовать. Шикарный набор параметров, не находите? 🙂 Куда рисовать, мы задаём через Rectange, а откуда — через 4 параметра. Ну это я так, лирическое отступление в сторону Microsoft.

Собственно, именно DrawImageTransparent и есть основной способ рисования изображений с прозрачными пикселями. Однако минус этого способа очевиден, состояния прозрачности всего два: полностью прозрачно и совсем непрозрачно.

Пример:

Compact Framework - Грани прозрачности
На самом деле, вполне неплохо, можно на этом и остановиться. Но хочется-то большего 🙂

Способ №2. У всего изображения фиксированный коэффициент непрозрачности

В этом случае без DllImport уже не обойтись, приготовим всё, что для этого необходимо:

public struct BlendFunction
{
  public byte BlendOp;
  public byte BlendFlags;
  public byte SourceConstantAlpha;
  public byte AlphaFormat;
}

public enum BlendOperation : byte
{
  AC_SRC_OVER = 0x00
}

public enum BlendFlags : byte
{
  Zero = 0x00
}

public enum SourceConstantAlpha : byte
{
  Transparent = 0x00,
  Opaque = 0xFF
}

public enum AlphaFormat : byte
{
  AC_SRC_ALPHA = 0x01
}

public class PlatformAPI
{
  [DllImport("coredll.dll")]
  extern public static Int32 AlphaBlend(IntPtr hdcDest, 
    Int32 xDest, Int32 yDest, Int32 cxDest, Int32 cyDest, 
    IntPtr hdcSrc, Int32 xSrc, Int32 ySrc, Int32 cxSrc, 
    Int32 cySrc, BlendFunction blendFunction);        
}

Как видно, обрезано всё, что только можно обрезать — в enum-ах по одному параметру и т.д. Но тем не менее, продолжаем. Собственно, наша функция:

public static void DrawAlpha(Graphics g, Bitmap b, Point location, byte opacity)
{
  if (b == null || g == null)
    return;

  using (Graphics gxSrc = Graphics.FromImage(g))
  {
    IntPtr hdcDst = g.GetHdc();
    IntPtr hdcSrc = gxSrc.GetHdc();
    BlendFunction blendFunction = new BlendFunction();
    blendFunction.BlendOp = (byte)BlendOperation.AC_SRC_OVER;
    blendFunction.BlendFlags = (byte)BlendFlags.Zero;
    blendFunction.SourceConstantAlpha = opacity;
    blendFunction.AlphaFormat = (byte)0;    
    PlatformAPI.AlphaBlend(hdcDst, location.X, location.Y, 
      b.Width, b.Height, hdcSrc, 0, 0, b.Width, b.Height, blendFunction);
    g.ReleaseHdc(hdcDst);
    gxSrc.ReleaseHdc(hdcSrc);
  }
}

Небольшие комментарии по коду — параметры у BlendFunction нельзя менять, они проставляются единственно возможные. Это обидно, но делать нечего.

Пример:
Compact Framework - грани прозрачности
Жутковато, да? Противные фиолетовые пиксели никуда не делись и тоже стали немного прозрачными 🙁

Комбинированное использование обоих способов

Вариантов комбинирования у нас, к сожалению, немного. На первый взгляд их совсем нет 🙂 Но есть всё-таки один способ.

Итак, решение следующее. Раз мы не можем одновременно задать ColorKey и вызвать AlphaBlend, будем использовать их по очереди. Сначала нарисуем фон стандартным спосбом без изысков, затем кнопку первым спосбом, а в конце… вторым спосбом нарисуем поверх фон с небольшим коэффициентом непрозрачности!

g.DrawImage(background, 0, 0);
DrawImageTransparent(g, button, new Point(10, 10), Color.FromArgb(255, 0, 255));
DrawAlpha(g, background, new Point(0, 0), 75);

Результат:
Compact Framework - Грани прозрачности

Описанный выше способ вполне жизнеспособен. Я им пользуюсь и вполне удовлетворён скоростью работы — на отрисовку всех элементов интерфейса в подобном стиле уходит в среднем от 60 до 80 миллисекунд (проверялось на разнообразных устройствах). Для создания приложения в таком стиле, безусловно, стандартные контролы не подойдут, но а кто обещал, что будет легко? В любом случае, для создания неописуемой красоты без собственного фреймворка рендеринга графических элементов не обойтись.

Оригинал статьи на Хабре.

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

This post has 2 Comments

2
  1. а можете уточнить пожалуйста что имело ввиду под этими словами “собственного фреймворка рендеринга графических элементов”?? что для красивого интерфейса не стоит использовать написаные вручную элементы управления? то есть лучше иметь один элемент управления и в нем все отрисовывать ручками?то есть не используя кучу специальных классических пользовательских элементов управления?

  2. Я думаю, здесь имелось в виду owner-drawn’ая версия каждого контрола. Хотя в принципе вариант когда одним контролом эмулируется работа всего интерфейса пользователя тоже не так и плох, для простых приложений это было бы нормально. В OpenGL-играх ведь тоже те же меню рендерятся в одной сцене на одном контроле.

Leave a Reply

Your email address will not be published. Required fields are marked *

Р.

Работаем с журналом звонков в Windows Mobile

В этот раз я расскажу о том, как работать с журналом звонков на С++ в Windows Mobile.

Для доступа к журналу звонков Windows Mobile имеет такую вещь, как Phone API.

Непосредственно для наших целей необходима всего небольшая часть функций, доступных в рамках Phone API, а именно:

  • PhoneOpenCallLog – открывает журнал звонков для чтения и возвращает хэндл, использующийся впоследствии для доступа к записям журнала.
  • PhoneGetCallLogEntry – получает данные о записи журнала звонков и заполняет структуру CALLLOGENTRY этими данными
  • PhoneCloseCallLog – закрывает хэндл журнала звонков.

Структура CALLLOGENTRY после успешного завершения работы функции PhoneGetCallLogEntryбудет содержать такую информацию:

  • Телефонный номер, на который был совершен звонок (если этот звонок исходящий) или с которого был совершен звонок (если звонок входящий)
  • Имя записи в адресной книге, соответствующей номеру
  • Тип номера (домашний/рабочий/мобильный), берется также из адресной книги
  • Дата и время начала звонка
  • Дата и время окончания звонка
  • Тип звонка (входящий, исходящий, пропущенный)
  • Флаг, указывающий на то, произошло ли соединение
  • Флаг, указывающий на то, был ли звонок завершен нормально или произошел обрыв
  • Флаг, указывающий на то, был ли использован роуминг.
  • Тип контакта (доступен/недоступен/заблокирован)
  • Текст заметки

Итак, давайте посмотрим, как это все работает. Пример для этой статьи написан с использованием wxWinCE. Главная форма приложения содержит list control со списком звонков, в котором указано имя контакта и с какого номера был произведен звонок.

Windows Mobile - Get Call Log Entries - C++ - Main Screen

При запуске приложения выполняется получения данных из журнала звонков:

void wxCallLogSampleMainFrame::FillCallLogList()
{
	m_CallLogListView->Freeze();
	do 
	{
		m_CallLog.Clear();
		HANDLE callLogHandle = INVALID_HANDLE_VALUE;
		HRESULT hr = PhoneOpenCallLog(&callLogHandle);
		if(FAILED(hr)) break;
		CALLLOGENTRY entry;
		entry.cbSize = sizeof(CALLLOGENTRY);
		do 
		{
			hr = PhoneGetCallLogEntry(callLogHandle, &entry);
			if(hr == S_OK)
			{
				wxCallLogEntry result;
				CreateCallLogEntry(entry, result);
				m_CallLog.Add(result);
			}
		} while (hr != S_FALSE);
		PhoneCloseCallLog(callLogHandle);
		m_CallLogListView->SetCallLogArray(&m_CallLog);
		m_CallLogListView->SetItemCount(m_CallLog.Count());
	} while (false);
	m_CallLogListView->Thaw();
}

Для хранения информации о звонке в виде, доступном для использования библиотекой wxWidgets мне пришлось написать собственный класс:

wxCallLogEntry.h

#ifndef _WX_CALL_LOG_ENTRY_H
#define _WX_CALL_LOG_ENTRY_H

#include <wx/wx.h>
#include <wx/dynarray.h>

enum wxCallType
{
	wxCALL_INCOMING,
	wxCALL_OUTGOING,
	wxCALL_MISSED
};

enum wxCallerIDType
{
	wxCALLER_ID_AVAILABLE,
	wxCALLER_ID_UNAVAILABLE,
	wxCALLER_ID_BLOCKED
};

class wxCallLogEntry : public wxObject
{
	DECLARE_DYNAMIC_CLASS(wxCallLogEntry)
public:
	wxCallLogEntry() {}
	wxCallLogEntry(const wxString & phoneNumber,
		const wxDateTime & startTime,
		const wxDateTime & endTime,
		const wxString & callName,
		const wxString & callNameType,
		wxCallType callType,
		wxCallerIDType callerIDType,
		bool wasConnected,
		bool wasEnded,
		bool roamingEnabled,
		const wxString & note = wxEmptyString);

	const wxString & GetPhoneNumber();
	void SetPhoneNumber(const wxString & value);

	const wxDateTime & GetStartTime();
	void SetStartTime(const wxDateTime & value);

	const wxDateTime & GetEndTime();
	void SetEndTime(const wxDateTime & value);

	const wxString & GetCallName();
	void SetCallName(const wxString & value);

	const wxString & GetCallNameType();
	void SetCallNameType(const wxString & value);
	
	wxCallType GetCallType();
	void SetCallType(wxCallType value);

	wxCallerIDType GetCallerIDType();
	void SetCallerIDType(wxCallerIDType value);

	bool GetConnected();
	void SetConnected(bool value);

	bool GetEnded();
	void SetEnded(bool value);

	bool GetRoamingEnabled();
	void SetRoamingEnabled(bool value);

	const wxString & GetNote();
	void SetNote(const wxString & value);
private:
	wxString m_PhoneNumber;
	wxDateTime m_StartTime;
	wxDateTime m_EndTime;
	wxString m_CallName;
	wxString m_CallNameType;
	wxCallType m_CallType;
	wxCallerIDType m_CallerIDType;
	bool m_Connected;
	bool m_Ended;
	bool m_RoamingEnabled;
	wxString m_Note;
};

WX_DECLARE_OBJARRAY(wxCallLogEntry, wxCallLogArray);

#endif

Код для преобразования данных из CALLLOGENTRY в wxCallLogEntry

void wxCallLogSampleMainFrame::CreateCallLogEntry(
	CALLLOGENTRY & entry, wxCallLogEntry & result)
{
	wxCallerIDType idType = wxCALLER_ID_UNAVAILABLE;
	switch(entry.cidt)
	{
	case CALLERIDTYPE_UNAVAILABLE:
		idType = wxCALLER_ID_UNAVAILABLE;
		break;
	case CALLERIDTYPE_BLOCKED:
		idType = wxCALLER_ID_BLOCKED;
		break;
	case CALLERIDTYPE_AVAILABLE:
		idType = wxCALLER_ID_AVAILABLE;
		break;
	default:
		break;
	};
	result.SetCallerIDType(idType);

	wxCallType callType = wxCALL_INCOMING;
	switch(entry.iom)
	{
	case IOM_INCOMING:
		callType = wxCALL_INCOMING;
		break;
	case IOM_OUTGOING:
		callType = wxCALL_OUTGOING;
		break;
	case IOM_MISSED:
		callType = wxCALL_MISSED;
		break;
	default:
		break;
	};
	result.SetCallType(callType);

	SYSTEMTIME systemTime;

	FileTimeToSystemTime(&entry.ftStartTime, &systemTime);
	wxDateTime startTime(TimeFromSystemTime(&systemTime));
	result.SetStartTime(startTime);
	
	FileTimeToSystemTime(&entry.ftEndTime, &systemTime);
	wxDateTime endTime(TimeFromSystemTime(&systemTime));
	result.SetEndTime(endTime);
	
	wxString number = ((entry.pszNumber != NULL) ? 
		wxString::Format(wxT("%s"),entry.pszNumber) :
	wxEmptyString);
	result.SetPhoneNumber(number);
	
	wxString name = ((entry.pszName != NULL) ? 
		wxString::Format(wxT("%s"),entry.pszName) :
	wxEmptyString);
	result.SetCallName(name);
	
	wxString nameType = ((entry.pszNameType != NULL) ? 
		wxString::Format(wxT("%s"),entry.pszNameType) :
	wxEmptyString);
	result.SetCallNameType(nameType);

	wxString note = ((entry.pszNote != NULL) ? 
		wxString::Format(wxT("%s"),entry.pszNote) :
	wxEmptyString);
	result.SetNote(note);

	result.SetConnected(entry.fConnected != 0);
	result.SetEnded(entry.fEnded != 0);
	result.SetRoamingEnabled(entry.fRoam != 0);
}

Структура CALLOGENTRY хранит дату и время начала и окончания звонка в виде структуры FILETIME. Для преобразования FILETIME в wxDateTime сначала необходимо выполнить преобразование в SYSTEMTIME с помощью функции FileTimeToSystemTime(), а затем в time_t:

time_t TimeFromSystemTime(const SYSTEMTIME * pTime)
{
	tm _tm;
	memset(&_tm, 0, sizeof(tm));

	_tm.tm_year = (pTime->wYear-1900);
	_tm.tm_mon = pTime->wMonth - 1;
	_tm.tm_mday = pTime->wDay;

	_tm.tm_hour = pTime->wHour;
	_tm.tm_min = pTime->wMinute;
	_tm.tm_sec = pTime->wSecond;

	return mktime(&_tm);
}

Для отображения данных о звонках я решил использовать виртуальный list control. Почему виртуальный, а не обычный? Потому что добавление большого количества элементов в список происходит довольно долго и пользователь может несколько секунд ждать того момента, когда все данные будут добавлены и приложение начнет как-то реагировать на его действия. При использовании виртуального списка вызывается метод SetItemCount(), который позволяет правильно вычислить размеры скроллеров. Это происходит довльно быстро. Затем виртуальный list control отображает только видимые элементы списка.

Для создания виртуального list control’а необходимо создать класс производный от wxListCtrl или wxListView и переопределить в нем методы:

virtual wxString OnGetItemText(long item, long column) const;
virtual int OnGetItemImage(long item) const;
virtual wxListItemAttr * OnGetItemAttr(long item) const;
  • OnGetItemText – используется для получения текста ячейки списка по указанным номеру строки и номеру колонки
  • OnGetItemImage – используется для получения индекса картинки элемента списка по указанному номеру строки
  • OnGetItemAttr – используется для получения атрибутов элемента списка (цвет текста, цвет фона, шрифт) по указанному номеру строки

И вот, собственно, код этих трех методов

wxString wxCallLogListView::OnGetItemText(long item, long column) const
{
	do 
	{
		if(!m_CallLogArray || 
			item >= (long)m_CallLogArray->Count() || 
			item >= GetItemCount()) break;
		switch(column)
		{
		case 2:
			return m_CallLogArray->Item(item).GetPhoneNumber();
		case 0:
			return m_CallLogArray->Item(item).GetCallName();
		case 1:
			return m_CallLogArray->Item(item).GetCallNameType();
		default:
			break;
		}
	} while (false);
	return wxEmptyString;
}

int wxCallLogListView::OnGetItemImage(long item) const
{
	do 
	{
		if(!m_CallLogArray || 
			item >= (long)m_CallLogArray->Count() || 
			item >= GetItemCount()) break;
		switch(m_CallLogArray->Item(item).GetCallType())
		{
		case wxCALL_INCOMING:
			return 1;
		case wxCALL_OUTGOING:
			return 2;
		case wxCALL_MISSED:
			return 3;
		}
	} while (false);
	return -1;
}

wxListItemAttr * wxCallLogListView::OnGetItemAttr(long item) const
{
	do 
	{
		if(!m_CallLogArray || 
			item >= (long)m_CallLogArray->Count() || 
			item >= GetItemCount()) break;
		return new wxListItemAttr(*wxBLACK, 
			(item%2) ? *wxWHITE : *wxLIGHT_GREY,
			GetFont());
	} while (false);
	return NULL;
}

Для того, чтобы в списке отображались иконки необходимо сначала создать объект wxImageList, добавить в него иконки и затем ассоциировать с list control’ом с помощью метода SetImageList()

static bool imageListCreated = false;
if(!imageListCreated)
{
	wxCallLogListView::ItemImageList.Create(16, 15);
	wxBitmap empty(16, 15);
	empty.SetMask(new wxMask(empty, *wxBLACK));
	wxCallLogListView::ItemImageList.Add(empty);
	wxCallLogListView::ItemImageList.Add(wxBitmap(forward_xpm));
	wxCallLogListView::ItemImageList.Add(wxBitmap(back_xpm));
	wxCallLogListView::ItemImageList.Add(wxBitmap(delete_xpm));
	imageListCreated = true;
}

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

BEGIN_EVENT_TABLE( wxCallLogSampleMainFrame, wxFrame )
    EVT_LIST_ITEM_ACTIVATED( ID_CallLogListView, wxCallLogSampleMainFrame::OnCallLogListViewItemActivated )
END_EVENT_TABLE()
void wxCallLogSampleMainFrame::OnCallLogListViewItemActivated( wxListEvent& event )
{
	CallInfoDialog * dlg = new CallInfoDialog(this);
	dlg->SetCallLogEntry(&m_CallLog[event.GetSelection()]);
	dlg->ShowModal();
	dlg->Destroy();
}

void CallInfoDialog::SetCallLogEntry(wxCallLogEntry * entry)
{
	if(!entry) return;
	m_PhoneNumberTextCtrl->SetValue(entry->GetPhoneNumber());
	m_CallNameTextCtrl->SetValue(entry->GetCallName());
	m_CallNameTypeTextCtrl->SetValue(entry->GetCallNameType());
	m_StartTextCtrl->SetValue(entry->GetStartTime().Format());
	m_EndTextCtrl->SetValue(entry->GetEndTime().Format());
	m_CallTypeRadio->SetSelection(entry->GetCallType());
	m_CallerTypeRadio->SetSelection(entry->GetCallerIDType());
	m_ConnectedCheck->SetValue(entry->GetConnected());
	m_EndedCheck->SetValue(entry->GetEnded());
	m_RoamingCheck->SetValue(entry->GetRoamingEnabled());
	m_NotesTextCtrl->SetValue(entry->GetNote());
}

В результате у нас должно получиться что-то подобное:
Windows Mobile - Show Call Log Entries - C++ - Show Details

Скачать исходный код к статье + проект для Windows Mobile 5.

W.

Windows Mobile Widgets – Новый тип приложений для Windows Mobile

Windows Mobile Widgets - MSN WidgetСегодня в блоге разработчиков Windows Mobile появилась информация о новом типе приложений, который будет доступен в Windows Mobile 6.5.

На данный момент для разработки приложений для Windows Mobile можно было использовать либо системное API (Native) либо управляемый код (Managed). В новой версии Windows Mobile ,удет доступна разработка приложений с использованием Web-технологий (HTML, CSS, AJAX, JavaScript).

Т.к. эти приложения будут использовать возможности новой версии Internet Explorer, то разработчики будут иметь доступ к использованию Flash и ActiveX компонентов, установленных на устройстве, таких, например, как MediaPlayer.

Заявлено также соответствие стандарту разработки мобильных widget-приложений от W3C, а также прозрачности для PNG-bзображений и нормальная работа с AJAX (support for the Window.XMLHttpRequest object work as expected).

Более подробно о новом типе приложений разработчики обещают рассказать на TechDays в апреле.