Как сделать привлекательный интерфейс для приложений Windows Mobile, использующих Microsoft .NET Compact Framework

Оригинал: https://msdn.microsoft.com/en-us/library/dd630622.aspx

В этой статье рассказывается, как использовать доступные в Windows Mobile API для смешивания по альфа-каналу и градиентной заливки, чтобы сделать интерфейс ваших приложений более привлекательными.

Скачать примеры: Msdn.UI_code.zip.

Продукты и технологии:

Windows Mobile 5.0 и старше

Microsoft .NET Compact Framework 3.5

Введение

За последние несколько лет аппаратура мобильных устройств, работающих под управлением Windows Mobile, была настолько усовершенствованна, что стало возможным создавать приложения с привлекательным, функциональным и насыщенным графикой интерфейсом. Необходимо, чтобы разработчики добавляли в платформу Windows Mobile такие креативные решения.

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

К этим API можно получить доступ из .NET Compact Framework. Используйте класс GraphicsExtension и приведенные примеры для того, чтобы упростить создание привлекательных интерфейсов.

Рисование прозрачных изображений

Для создания прозрачной картинки или иконки наиболее часто используется следующий подход – фон изображения закрашивается каким-либо цветом, и этот цвет считается прозрачным. На управляемом коде это можно сделать очень просто – использовать метод SetColorKey(Color, Color), этот метод описан в .NET Compact Framework Developer Guide на MSDN.

Другой подход, который тоже используется очень часто, состоит в следующем. Все изображение считается прозрачным и выглядит так, как будто вы смотрите через него. Такой эффект получается благодаря технике, называемой «смешиванием по альфа-каналу (alpha blending). Смешивание по альфа-каналу – это процесс объединения полупрозрачного изображения и непрозрачного фона, в результате чего получается новый «смешанный» цвет, который и соответствует полупрозрачным частям изображения. Windows Mobile API включает функцию AlphaBlend, с помощью которой к изображению можно применить альфа-канал. Если же изображение уже содержит готовый альфа-канал, то его можно применить, используя интерфейс Imaging API, который включен в Windows Mobile, начиная с версии 5.0.

Крис Лортон (Chris Lorton), один из членов команды разработчиков .NET Compact Framework, описал в своем блог-посте требуемый набор объявлений для использования AlphaBlend и Imaging API через P/Invoke. Код примера Криса был использован при написании класса PlatformAPIs, его можно найти в исходном коде, который прилагается к этой статье.

Рисование градиента

Другой способ добавить изюминку в интерфейс приложения – это использовать градиентную заливку. Градиентная заливка создается смешиванием двух или нескольких цветов, причем один цвет плавно переходит в другой. Нарисовать градиент очень просто – для этого есть функция GradientFill. Пример использования этой функции приведен в статье на MSDN, How to Display a Gradient Fill. Часть кода этого примера использовалась при написании класса GradientFill в примерах к этой статье (Msdn.UI_code.zip).

Расширение графических возможностей

Теперь у вас есть вся необходимая информация, чтобы создать что-то более полезное. Для этого можно использовать методы расширения, это новая функциональность, которая была добавлена в .Net Framework 3.5. Методы расширения позволяют добавлять новые методы в публичный контракт существующего типа Common Language Runtime (CLR). Так, класс System.Drawing.Graphics может быть расширен. Пример кода ниже иллюстрирует использование этих двух методов.

 /// <summary>
/// Draws an image with transparency
/// </summary>
/// <param name="gx">Graphics to drawn on.</param>
/// <param name="image">Image to draw.</param>
/// <param name="transparency">Transparency constant</param>
/// <param name="x">X location</param>
/// <param name="y">Y location</param>
public static void DrawAlpha(
    this Graphics gx,
    Bitmap image,
    byte transparency,
    int x,
    int y)
{
    using (Graphics gxSrc = Graphics.FromImage(image))
    {
        IntPtr hdcDst = gx.GetHdc();
        IntPtr hdcSrc = gxSrc.GetHdc();
 
        BlendFunction blendFunction = new BlendFunction();
 
        // Only supported blend operation
        blendFunction.BlendOp = (byte)BlendOperation.AC_SRC_OVER;
 
        // Documentation says put 0 here
        blendFunction.BlendFlags = (byte)BlendFlags.Zero;
 
        // Constant alpha factor
        blendFunction.SourceConstantAlpha = transparency;
 
        // Don't look for per pixel alpha
        blendFunction.AlphaFormat = (byte)0;
 
        PlatformAPIs.AlphaBlend(
            hdcDst,
            x,
            y,
            image.Width,
            image.Height,
            hdcSrc,
            0,
            0,
            image.Width,
            image.Height,
            blendFunction);
 
        // Required cleanup to GetHdc()
        gx.ReleaseHdc(hdcDst);
 
        // Required cleanup to GetHdc()
        gxSrc.ReleaseHdc(hdcSrc);
    }
}
 /// <summary>
/// Fills the rectagle with gradient colors
/// </summary>
/// <param name="gx">Destination graphics</param>
/// <param name="rc">Desctination rectangle</param>
/// <param name="startColorValue">Starting color for gradient</param>
/// <param name="endColorValue">End color for gradient</param>
/// <param name="fillDirection">The direction of the gradient</param>
public static void FillGradientRectangle(
    this Graphics gx,
    Rectangle rc,
    Color startColorValue,
    Color endColorValue,
    FillDirection fillDirection)
{
    GradientFill.Fill(
        gx,
        rc,
        startColorValue,
        endColorValue,
        fillDirection);
}

Как видите, методы DrawAlpha и FillGradientRectangle являются расширением класса Graphics, и их можно использовать в коде, который выполняет рисование. Эти методы включены в класс GraphicsExtension, который является составной часть примера, приложенного к этой статье. Следующий фрагмент кода показывает, как использовать эти методы.

 private void Form1_Paint(object sender, PaintEventArgs e)
{
    Rectangle rectangle = new Rectangle(0, 0, 200, 100);
 
    // Create temporary bitmap
    Bitmap bitmap = new Bitmap(200,
            100);
 
    // Create temporary graphics
    Graphics tempGraphics
        = Graphics.FromImage(bitmap);
 
    // Draw a red rectangle in the temp graphics object
    using (Brush brush = new SolidBrush(Color.Red))
    {
        tempGraphics.FillRectangle(brush, rectangle);
    }
 
    // Draw a gradient background on the form
    e.Graphics.FillGradientRectangle(
        this.ClientRectangle,
        Color.Blue,
        Color.LightBlue,
        FillDirection.TopToBottom);
 
    // Draw temp bitmap with alpha transparency
    e.Graphics.DrawAlpha(bitmap, 60, 20, 100);
}

Этот код закрашивает форму градиентом и рисует красный прямоугольник с полупрозрачным верхом. Результат показан ниже.

Класс GraphicsExtension содержит несколько чрезвычайно полезных методов для рисования прямоугольников со скругленным углами, причем такие прямоугольники можно залить градиентом или сделать полупрозрачными. Это методы FillGradientRectangle, DrawRoundedRectangleAlpha и некоторые другие. Вы можете поэкспериментировать с этими «вкусностями» класса GraphicsExtension.

Следующий фрагмент кода иллюстрирует использование метода DrawGradientRoundedRectangle.

 private void Form1_Paint(object sender, PaintEventArgs e)
{
    // Prepare rectangle
    Rectangle rectangle = new Rectangle(20, 100, 200, 30);
 
    // Draw a gradient background on the form
    e.Graphics.FillGradientRectangle(
        this.ClientRectangle,
        Color.Blue,
        Color.LightBlue,
        FillDirection.TopToBottom);
 
    e.Graphics.DrawGradientRoundedRectangle(
        rectangle,
        Color.Red,
        Color.LightPink,
        Color.Red,
        new Size(6, 6));
}

И вот результат:

Правила рисования

Если вы внимательно ознакомились с приведенными выше примерами, вы должны были заметить, что для создания полупрозрачности в Windows Mobile приложениях соблюдается определенное правило: все рисование происходит в одном графическом контексте. Это продиктовано тем фактом, что Windows Mobile не поддерживает прозрачность на уровне окон. Если же вы попытаетесь создать элемент управления COM, унаследованный от класса System.Windows.Forms.Control, то столкнетесь с массой проблем, когда захотите сделать этот элемент управления полупрозрачным относительно формы-контейнера.

Кроме этого, не следует забывать, что, несмотря на то, что мобильные устройства становятся все более совершенными, объемы памяти и скорость процессора пока не выдерживают никакого сравнения с аналогичными ресурсами настольных компьютеров. Каждый дополнительный графический контекст, кисть, или даже простая операция рисования занимает процессорное время и ресурсы. Наиболее оптимальный способ – это использование единственного экземпляра объекта System.Drawing.Graphics.

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

Вооруженный этими знаниями и классом GraphicsExtension, я создал простой UI framework, который вы можете использовать как отправную точку для ваших собственных приложений под .NET Compact Framework.

Начинаем с класса UIElement

Этот framework построен на следующих допущениях:

  • Каждый элемент управления должен сам заниматься своей отрисовкой.
  • Чтобы элемент мог нарисовать себя, ему должен быть передан объект Graphics.

Посмотрите на класс UIElement. Это базовый класс, который используется для создания элементов ImageElement, TextBlock и Border. Этот класс реализует набор событий, свойств и следующих методов.

 /// <summary>
/// Renders the UIElement.
/// </summary>
/// <param name="graphics">Graphics object.</param>
public void Render(Graphics graphics)
{
    this.OnRender(graphics);
}
 
/// <summary>
/// Render method to implement by inheritors
/// </summary>
/// <param name="graphics">Graphics object.</param>
protected virtual void OnRender(Graphics graphics)
{
 
}

Как видите, метод Render может быть вызван из формы или элемента управления, и ему должен быть передан экземпляр объекта Graphics. Следовательно, виртуальный метод OnRender непременно будет вызван из определенного класса, который реализует этот метод. Следующий код показывает, как класс ImageElement реализует метод OnRender.

 protected override void OnRender(Graphics graphics)
{
    if (this.image != null)
    {
        if (transparentBackground)
        {
            graphics.DrawImageTransparent(
                this.image,
                this.Rectangle);
        }
        else if (alphaChannel)
        {
            graphics.DrawImageAlphaChannel(
                this.imagingImage,
                this.Left,
                this.Top);
        }
        else if (stretch)
        {
            graphics.DrawImage(
                this.image,
                this.Rectangle,
                new Rectangle(0, 0, this.image.Width, this.image.Height),
                GraphicsUnit.Pixel);
        }
        else
        {
            graphics.DrawAlpha(
                this.image,
                this.Opacity,
                this.Left,
                this.Top);
        }
    }
}

Используя этот метод, вы получаете все преимущества методов класса GraphicsExtension для рисования прозрачных изображений или изображений с альфа-каналом. Пример кода ниже показывает тот же самый метод OnRender в классе TextBlock.

 protected override void OnRender(Graphics drawingContext)
{
    if (this.Text != null)
    {
        Size sizeContent = Size.Empty;
 
        // Prepare font
        Font font = new Font(
            this.fontFamily,
            this.fontSize,
            fontStyle);
 
        // Measure the string
        SizeF sizeContentF = drawingContext.MeasureString(
            this.Text,
            font);
        sizeContent.Width = (int)sizeContentF.Width;
        sizeContent.Height = (int)sizeContentF.Height;
 
        // Calculate the location of the text
        Point point = GetLocationFromContentAlignment(
            this.alignment,
            sizeContent);
 
        // Draw the text
        using (Brush brushText = new SolidBrush(this.Foreground))
        {
            Rectangle textRect = new Rectangle(
                point.X,
                point.Y,
                this.Width - 1,
                this.Height - 1);
            drawingContext.DrawString(
                this.Text,
                font,
                brushText,
                textRect);
        }
 
        // Clean up
        font.Dispose();
    }
}

Создание хоста

Чтобы использовать доступные во framework элементы интерфейса, нужно иметь корректный класс формы или элемента управления, который позволит элементам использовать объект Graphics. Следующий фрагмент кода показывает добавленный класс UIForm, который является наследником класса System.Windows.Form и переопределяет метод OnPaint.

 public class UIForm : Form
{
    private Image backgroundImage;
    private Bitmap offBitmap;
    private Graphics offGx;
    private Canvas canvas;
 
    public UIForm()
    {
        this.canvas = new Canvas(this);
 
        // Don't show title bar and allocate the full screen
        this.FormBorderStyle = FormBorderStyle.None;
        this.WindowState = FormWindowState.Maximized;
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        // Draw a background first
        this.offGx.Clear(this.BackColor);
 
        // Draw background image
        if (this.backgroundImage != null)
        {
            this.offGx.DrawImage(this.backgroundImage, 0, 0);
        }
 
        // Pass the graphics to the canvas to render
        if (this.canvas != null)
        {
            this.canvas.Render(offGx);
        }
 
        // Blit the offscreen bitmap
        e.Graphics.DrawImage(offBitmap, 0, 0);
    }
}

В приведенном примере я использую встроенный во framework класс Canvas. Этот класс является родителем для всех элементов интерфейса. Он держит их экземпляры в VisualCollection и передает вызов метода Render каждому из элементов интерфейса в коллекции. В следующем примере кода видно, на что похож метод Render в классе Canvas.

 protected override void OnRender(Graphics graphics)
{
    // Draw a back color
    if (this.Background != null)
    {
        using (Brush brush = new SolidBrush(Background))
        {
            graphics.FillRectangle(brush, new Rectangle(
                this.Left,
                this.Top,
                this.Height,
                this.Width));
        }
    }
 
    // Pass the graphics objects to all UI elements
    for (int i = 0; i < children.Count; i++)
    {
        UIElement element = children[i];
        if (element.Visible)
        {
            element.Render(graphics);
        }
    }
}

Нестандартный заголовок и строка меню

Поскольку мы создаем класс UIForm, который позволяет приложению разворачиваться на весь экран, нам следует сделать для формы нестандартный заголовок и строку меню. Классы TitleBar и MenuBar продекларированы в UIForm, в том же проекте Msdn.UIFramework. Конечно же, оба класса являются наследниками UIElement, и метод Render реализует отрисовку элементов интерфейса. Сделать заголовок и стороку меню нестандартными можно, изменив цвет фона или цвета градиента. Если настроить соответствующие свойства, TitleBar и MenuBar можно сделать доступными из UIForm.

Пример

Чтобы показать вам, как использовать UI framework, я создал небольшой пример, который вы можете найти в проекте Msdn.TestClient. Тут есть класс DemoForm, наследник класса UIForm. Пример кода ниже показывает, как выглядит конструктор класса DemoForm.

 public DemoForm()
{
    InitializeComponent();
 
    // Set gradient for title bar
    this.TitleBar.Fill = new LinearGradient(
        Color.DarkBlue,
        Color.CornflowerBlue);
    this.TitleBar.Text = this.Text;
 
    // Set values for menu
    this.MenuBar.LeftMenu = "Exit";
    this.MenuBar.RightMenu = "Location";
 
    this.InitializeWeatherUI("75", "Redmond, WA", 50, 50);
    this.InitializeSlideShow(50, 140, "Garden.jpg");
}

В этом конструкторе для TitleBar настраивается произвольный градиент, а так же задаются некоторые текстовые значения для свойств MenuBar - LeftMenu и RightMenu. Кроме этого, чтобы продемонстрировать возможности UI framework, я решил воспроизвести внешний вид стандартных гаджетов из Windows Vista – погоды и демонстрации слайдов. Я «позаимствовал» изображения гаджетов и включил их в проект Msdn.TestClient , в папку Images. Эти картинки полупрозрачные, поэтому они позволят нам провести прекрасный тест возможностей UI framework. И теперь вам должно быть понятно, зачем в прошлом примере в конструкторе вызывались методы InitializeWeatherUI и InitializeSlideShow. Посмотрите, как сделан один из этих методов.

 private void InitializeSlideShow(
    int left,
    int top,
    string imageFile)
{
    this.Canvas.AddElement<ImageElement>(
        () => new ImageElement()
        {
            Source = @"Images\slideshow_glass_frame.png",
            Top = top,
            Left = left,
            AlphaChannel = true,
        });
 
    this.Canvas.AddElement<ImageElement>(
        () => new ImageElement()
        {
            Name = "Slide",
            Source = @"Images\" + imageFile,
            Top = top + 5,
            Left = left + 4,
            Stretch = true,
            Width = 120,
            Height = 90
        });
}

Как видите, метод InitializeSlideShow принимает несколько параметров для того, чтобы установить положение и задать файл картинки для ImageElement. Используя метод AddElement, добавляем в класс Canvas два ImageElement. Метод AddElement декларируется в классе Canvas следующим образом:

 public T AddElement<T>(Func<T> buildFunc)
    where T : UIElement
{
    if (buildFunc == null)
        throw new ArgumentNullException("buildFunc");
 
    T instance = buildFunc();
    this.children.Add(instance as UIElement);
    return instance;
}

У метода AddElement есть параметр делегата Func, который принимает экземпляр UIElement. Это дает возможность использовать инициализаторы объекта в момент создания экземпляра ImageElement.

Чтобы воспроизвести гаджет погоды, можно использовать набор ImageElement, а так же добавить классы TextBlock, которые будут показывать название города и текущую температуру.

 private void InitializeWeatherUI(
    string temperature,
    string city,
    int left,
    int top)
{
    this.Canvas.AddElement<ImageElement>(
        () => new ImageElement()
        {
            Source = @"Images\BLUEDOCKED-base.png",
            Top = top,
            Left = left,
            AlphaChannel = true,
        });
 
    this.Canvas.AddElement<ImageElement>(
        () => new ImageElement()
        {
            Source = @"Images\docked_sun.png",
            Top = top,
            Left = left,
            AlphaChannel = true,
        });
 
    // Display temperature
    this.Canvas.AddElement<TextBlock>(
        () => new TextBlock()
        {
            Text = temperature + Convert.ToChar(176),
            Top = top + 2,
            Left = left + 97,
            FontSize = 12f,
            FontStyle = FontStyle.Bold
        });
 
    // Display city
    this.Canvas.AddElement<TextBlock>(
        () => new TextBlock()
        {
            Text = city,
            Top = top + 40,
            Left = left + 40,
            Width = 200,
        });
}

Ну и, наконец, вот что получилось. Так выглядит запущенное приложение.

Заключение

В этой статье я показал, как, используя некоторые полезные API для рисования, доступные в Windows Mobile, можно расширить возможности класса Graphics из .NET Compact Framework. Также я описал созданный мной UI framework демонстрирующий возможности Windows Mobile. Конечно, этот framework нельзя назвать полностью готовым к выпуску, его следует рассматривать только как пример, который вы можете использовать и дорабатывать для своих собственных нужд.

Об авторе

Алекс Яхнин (Alex Yakhnin) – старший консультант по мобильным устройствам группы Mobility Global Practices в компании Microsoft. Перед тем, как пойти работать в Microsoft, Алекс был архитектором и разработчиком крупных и небольших систем для финансовых компаний. Алекс написал несколько статей для MSDN. Много лет Алекс был .NET Compact Framework/Device Application Development MVP, а также участвовал в инициативе OpenNETCF.

Другие ресурсы

How to Create a Microsoft .NET Compact Framework-based Image Button

Chris Lorton's Blog

How to Display Gradient Fill

Alex Yakhnin's Blog

Перевод: Светлана Шиблева