четверг, 28 мая 2009 г.

Шаблоны в WPF

В основе идеи шаблонов лежит концепция отделения функциональности от внешнего вида.
Посмотрите на вот этот рисунок:

Как видно на кнопке размещается CheckBox, картинка, набор RadioButton и даже еще одна кнопка! И все это, как не странно, работает. Достигается это за счет того, что основная масса компонентов имеет свойство Content, в рамках которого мы можем создавать практически любое наполнение. Основной недостаток, мы не можем переопределить саму базовую кнопку. Например, сделать ее треугольной или круглой, хотя цвет, размер и много другое изменить достаточно легко.
Для изменения всего способа отображения и используются шаблоны.
В WPF существует несколько видов шаблонов:
1. ControlTemplate - позволяет задавать шаблон для любого визуального компонента.
2. ItemsPanelTemplate - позволяет задавать шаблон компоновки для контейнеров.
3. DataTemplate - задает шаблоны отображения данных.
4. HierarchicalDataTemplate - задает шаблоны древовидных структур.
Рассмотрим самый простой случай ControlTemplate:
  1.   <Window.Resources>
  2.     <Style TargetType="{x:Type Button}">
  3.       <Setter Property="Background" Value="Black" />
  4.       <Setter Property="Height" Value="40" />
  5.       <Setter Property="Foreground" Value="White" />
  6.       <Setter Property="Margin" Value="3" />
  7.       <Setter Property="Template">
  8.         <Setter.Value>
  9.           <ControlTemplate TargetType="{x:Type Button}">
  10.             <Grid>
  11.               <Rectangle Name="GelBackground" RadiusX="9" RadiusY="9" Fill="Black">                
  12.               </Rectangle>
  13.               <Rectangle Name="GelShine" Margin="2,2,2,0" VerticalAlignment="Top" RadiusX="6" RadiusY="6" Height="15px">
  14.                 <Rectangle.Fill>
  15.                   <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
  16.                     <GradientStop Offset="0" Color="#ccffffff" />
  17.                     <GradientStop Offset="1" Color="Transparent" />
  18.                   </LinearGradientBrush>
  19.                 </Rectangle.Fill>
  20.               </Rectangle>
  21.               <ContentPresenter Name="GelButtonContent" VerticalAlignment="Center" HorizontalAlignment="Center" Content="{TemplateBinding Content}" />
  22.             </Grid>
  23.             <ControlTemplate.Triggers>
  24.               <Trigger Property="IsMouseOver" Value="True">
  25.                 <Setter Property="Rectangle.Fill" TargetName="GelBackground">
  26.                   <Setter.Value>
  27.                     <RadialGradientBrush>
  28.                       <GradientStop Offset="0" Color="Lime" />
  29.                       <GradientStop Offset="1" Color="DarkGreen" />
  30.                     </RadialGradientBrush>
  31.                   </Setter.Value>
  32.                 </Setter>
  33.                 <Setter Property="Foreground" Value="Black" />
  34.               </Trigger>
  35.               <Trigger Property="IsPressed" Value="True">
  36.                 <Setter Property="Rectangle.Fill" TargetName="GelBackground">
  37.                   <Setter.Value>
  38.                     <RadialGradientBrush>
  39.                       <GradientStop Offset="0" Color="#ffcc00" />
  40.                       <GradientStop Offset="1" Color="#cc9900" />
  41.                     </RadialGradientBrush>
  42.                   </Setter.Value>
  43.                 </Setter>
  44.               </Trigger>
  45.             </ControlTemplate.Triggers>
  46.           </ControlTemplate>
  47.         </Setter.Value>
  48.       </Setter>
  49.     </Style>
  50.   </Window.Resources>
  51.   <Grid>
  52.     <Button Width="124">Кнопка с шаблоном</Button>
  53.   </Grid>
* This source code was highlighted with Source Code Highlighter.


Как видно мы задаем новый стиль (строка 2), который переопределяет свойство Template - как раз тот самый шаблон (строка 7 и далее). Все остальное достаточно понятно, кроме наверно строки 17 где цвет задается не как конкретный цвет (напрмер - Red), а как переход к цвету подложки - Transparent. И конечно строка 21 в которой мы задаем, что в нашем шаблоне кнопки в качестве контента должно быть то, что помещено в контент кнопки к которой применен шаблон. В строках 24 и 25 задаются тригеры для изменения цвета кнопки при наведении мышки и при нажатии.
Вот так все это выглядит в обычном режиме, при наведении мышки и нажатии.

Кстати, попробуйте сделать круглую кнопку ;)

вторник, 26 мая 2009 г.

Взаимодействие WF с программным окружением

Продолжим разработку нашего примера с workflow. В данной части мы посмотрим как из WF взаимодествовать с программным окружением. В качестве конечного автомата возьмем конечный автомат от сюда.
В данном примере, для наглядности, давайте создадим приложение с GUI:

Кнопки соответствуют событиям, прямоугольники показывают состояние двери и замка (красный - закрыто, зеленый открыто).
Для взаимодействия с программным окружением используются следующие Activity:
1. СallExternalMethodActivity – используется для вызова метода внешнего по отношению к WF (основные свойства InterfaceType и MethodName)

2. EventDrivenActivity – используется для перехвата событий из кода внешнего по отношению к WF
3. HandleExternalEventActivity – помещается в EventDrivenActivity и ожидает события (основные свойства InterfaceType и EventName)


К событиям передаваемым из внешнего окружения в WF предъявляются следующие требования:
1. Событие должно быть потомок EventHandler
2. В качестве T должен быть любой класс потомок ExternalDataEventArgs
Кстати, конструктор ExternalDataEventArgs принимает Guid потока которому предназначается сообщение.

Для взаимодействия с WF объекты должны являться, во-первых, наследниками интерфейса помеченного атрибутом [ExternalDataExchange].
Введем два интерфейса:
  [ExternalDataExchange]
  public interface IDoor
  {
    void OpenLock();
    void OpenDoor();
    void CloseDoor();
    string GetKey();
  }
  [ExternalDataExchange]
  public interface IVisitor
  {
    event EventHandler<ExternalDataEventArgs> TestKey;
    event EventHandler<ExternalDataEventArgs> Open;
    event EventHandler<ExternalDataEventArgs> Close;
  }

* This source code was highlighted with Source Code Highlighter.

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

Кстати, если методо вызываемый из WF возвращает значение, то его можно непосредственно в дизайнере привязать к полю WF, использовав для этгого свойство ReturnValue:


Добавив везде где это надо вместо CodeActivity вызов внешних методов и перехват внешних событий, получим конечный автомат вида:


Реализация интерфейса IDoor может иметь вид:
    #region IDoor Members
    public void OpenLock()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(OpenLock), new object[] { });
      }
      else
      {
        rcDoorlock.Fill = new SolidColorBrush(Color.FromRgb(0, 255, 0));
      }
    }
    public void OpenDoor()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(OpenDoor), new object[] { });
      }
      else
      {
        rcDoor.Fill = new SolidColorBrush(Color.FromRgb(0, 255, 0));
      }
    }
    private delegate void NoParamHandler();
    public void CloseDoor()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(CloseDoor), new object[] { });
      }
      else
      {
        rcDoorlock.Fill = new SolidColorBrush(Color.FromRgb(255, 0, 0));
        rcDoor.Fill = new SolidColorBrush(Color.FromRgb(255, 0, 0));
      }
    }
    public string GetKey()
    {
      if (Dispatcher.Thread == Thread.CurrentThread)
      {
        return tbKey.Text;
      }
      else
      {
        return (string)Dispatcher.Invoke(new Func<string>(GetKey), new object[] { });
      }
    }
    #endregion


* This source code was highlighted with Source Code Highlighter.


Ну и в завершении как же создать host для WF. На самом деле все достаточно просто:
  1.     WorkflowRuntime workflowRuntime;
  2.     WorkflowInstance instance;
  3.     StateMachineWorkflowInstance machine;
  4.  
  5.     private void Window_Loaded(object sender, RoutedEventArgs e)
  6.     {
  7.       workflowRuntime = new WorkflowRuntime();
  8.       workflowRuntime.WorkflowStarted += delegate(object s, WorkflowEventArgs e1) { MessageBox.Show("Рабочий поток начал работу"); };
  9.       workflowRuntime.WorkflowCompleted += delegate(object s, WorkflowCompletedEventArgs e1) { MessageBox.Show("Рабочий поток закончил работу"); };
  10.       workflowRuntime.WorkflowTerminated += delegate(object s, WorkflowTerminatedEventArgs e1)
  11.       {
  12.         MessageBox.Show(e1.Exception.Message);
  13.       };
  14.       ExternalDataExchangeService externalDataExchangeService = new ExternalDataExchangeService();
  15.       workflowRuntime.AddService(externalDataExchangeService);
  16.       externalDataExchangeService.AddService(this);
  17.       
  18.       instance = workflowRuntime.CreateWorkflow(typeof(Workflow1));
  19.       machine = new StateMachineWorkflowInstance(workflowRuntime, instance.InstanceId);
  20.       instance.Start();
  21.  
  22.     }
* This source code was highlighted with Source Code Highlighter.


В строках 7-13 мы создаем среду выполнения WF и подписываемся на основные ее обработчики.
В строках 14-16 создаем сервис для взаимодействия с WF, регистрируем его в среде выполнения, и добавляем объект методы которого необходимо вызывать и события обрабатывать (в данном случае это текущая форма).
В строках 18,20 создается WF и запускается на выполнение.
В строке 19 получаем ссылку на WF в виде StateMachine которую можно использовать для проверки текущего состояния конечного автоамта, принудительного перевода из состояния в состояние и т.д.

четверг, 21 мая 2009 г.

Статьи про работу с TaskBar в Windows 7

Программируем Windows 7: Taskbar. Часть 1 — Progress Bar
Программируем Windows 7: Taskbar. Часть 2 — ThumbButtons
Программируем Windows 7: Taskbar. Часть 3 – OverlayIcon
Программируем Windows 7: Taskbar. Часть 4 – Custom OverlayIcon
Программируем Windows 7: Taskbar. Часть 5 – CustomWindowsManager
Программируем Windows 7: Taskbar. Часть 6 – AppId
Программируем Windows 7: Taskbar. Часть 7 – ThumbnailClip
Программируем Windows 7: Taskbar. Часть 9 – PeekBitmap
Программируем Windows 7: Taskbar. Часть 10 (заключительная) – JumpLists

Триггеры в WPF

Триггер(англ. trigger), спусковое устройство (спусковая схема), которое может сколь угодно долго находиться в одном из двух (реже многих) состояний устойчивого равновесия и скачкообразно переключаться из одного состояния в другое под действием внешнего сигнала. (Большая советская энциклопедия).

Собственнов в этом определении сказано все, что нужно для понимания идеи триггеров.
Есть состояние -> происходит событие -> состояние меняется.
Посмотрим простенький пример:

  1. <Style TargetType="{x:Type TextBox}">
  2.       <Setter Property="TextBox.Background">
  3.         <Setter.Value>
  4.           <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  5.             <GradientStop Offset="0.0" Color="LightCyan" />
  6.             <GradientStop Offset="0.14" Color="Cyan" />
  7.             <GradientStop Offset="0.7" Color="DarkCyan" />
  8.           </LinearGradientBrush>
  9.         </Setter.Value>
  10.       </Setter>
  11.       <Style.Triggers>
  12.         <Trigger Property="IsFocused" Value="True">
  13.           <Setter Property="TextBox.Background">
  14.             <Setter.Value>
  15.               <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  16.                 <GradientStop Offset="0.0" Color="LightSeaGreen" />
  17.                 <GradientStop Offset="0.14" Color="SeaGreen" />
  18.                 <GradientStop Offset="0.7" Color="DarkSeaGreen" />
  19.               </LinearGradientBrush>
  20.             </Setter.Value>
  21.           </Setter>
  22.         </Trigger>
  23.       </Style.Triggers>
  24.     </Style>
* This source code was highlighted with Source Code Highlighter.


Итак, в первой строке задается картинка, для которой во второй строке мы открываем задание набора тригеров. Тригеры у нас заданы в строках 3 и 11. Превый привязан к событию захода мышки в компонент, второй к покиданию мышкой компонента.
В связи с тем, что свойство Width компонента Image имеет тип double мы и применяем в строках 6 и 14 DoubleAnimation. Задав целевое свойство (Storyboard.TargetProperty), продолжительность анимации (Duration) и к какому значению надо привести (To). В результете при навидении мышки на картинку она будет увеличиваться в размере.
Т.к. для каждой картинки в приложении особенно этого не наделаешься, то само собой можно применить стиль...
Например, вот такой:

  1. <Style TargetType="{x:Type Image}">
  2.       <Setter Property="Image.Width" Value="100"></Setter>
  3.       <Style.Triggers>
  4.         <EventTrigger RoutedEvent="Image.MouseEnter">
  5.           <BeginStoryboard>
  6.             <Storyboard >
  7.               <DoubleAnimation Storyboard.TargetProperty="Width" BeginTime="00:00:00" Duration="00:00:01"   From="100" To="200">
  8.               </DoubleAnimation>
  9.             </Storyboard>
  10.           </BeginStoryboard>
  11.         </EventTrigger>
  12.         <EventTrigger RoutedEvent="Image.MouseLeave">
  13.           <BeginStoryboard>
  14.             <Storyboard >
  15.               <DoubleAnimation Storyboard.TargetProperty="Width" BeginTime="00:00:00" Duration="00:00:01"    To="100">
  16.               </DoubleAnimation>
  17.             </Storyboard>
  18.           </BeginStoryboard>
  19.         </EventTrigger>
  20.       </Style.Triggers>
  21.     </Style>
* This source code was highlighted with Source Code Highlighter.


Теперь все картинки не имеющие переопределений стиля по умолчании будут иметь ширину 100, при наведнии мышки уширяться ;), а при покидании сжиматься...
Не претендуя на изыски можно, например, вот так организовать галлерею:

  1. <StackPanel Orientation="Horizontal">
  2.       <Image Source="Chrysanthemum.jpg"></Image>
  3.       <Image Source="Desert.jpg"></Image>
  4.       <Image Source="Hydrangeas.jpg"></Image>
  5.       <Image Source="Jellyfish.jpg"></Image>
  6.       <Image Source="Koala.jpg"></Image>
  7.       <Image Source="Lighthouse.jpg"></Image>
  8.       <Image Source="Penguins.jpg"></Image>
  9.       <Image Source="Tulips.jpg"></Image>
  10.     </StackPanel>
* This source code was highlighted with Source Code Highlighter.


Ну а в случае, когда нам анимация изменения не нужна, можно применять "не евентовые" тригеры, которые отслеживают изменение свойтсв:

  1. <Style TargetType="{x:Type TextBox}">
  2.       <Setter Property="TextBox.Background">
  3.         <Setter.Value>
  4.           <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  5.             <GradientStop Offset="0.0" Color="LightCyan" />
  6.             <GradientStop Offset="0.14" Color="Cyan" />
  7.             <GradientStop Offset="0.7" Color="DarkCyan" />
  8.           </LinearGradientBrush>
  9.         </Setter.Value>
  10.       </Setter>
  11.       <Style.Triggers>
  12.         <Trigger Property="IsFocused" Value="True">
  13.           <Setter Property="TextBox.Background">
  14.             <Setter.Value>
  15.               <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  16.                 <GradientStop Offset="0.0" Color="LightSeaGreen" />
  17.                 <GradientStop Offset="0.14" Color="SeaGreen" />
  18.                 <GradientStop Offset="0.7" Color="DarkSeaGreen" />
  19.               </LinearGradientBrush>
  20.             </Setter.Value>
  21.           </Setter>
  22.         </Trigger>
  23.       </Style.Triggers>
  24.     </Style>
* This source code was highlighted with Source Code Highlighter.


Интерес представляют строки с 11 по 23. Как видно в строке 12 задается тригер который отслеживает состояние свойства IsFocused и как только оно станет равно True к TextBox-у будут применен новый Setter. Причем в данном случае заботиться об обратной смене значений не необходимости. При возврате свойства IsFocused в false значение Setter-а будет отменено и фон станет таким каким был по умолчанию.

среда, 20 мая 2009 г.

Пример создания простого конечного автомата

Для создания конечного автомата можно воспользоваться уже существующим шаблоном Visual Studio - "Консольное приложение рабочих процессов конечного автомата".
В результате будет создано консольное приложение включающее конечный автомат и класс содержащий метод Main в который уже добавлен весь код необходимый для запуска конечного автомата.
Основным элементом для построения конечных автоматов является компонент: StateActivity, который и задает состояния конечного автомата.
В качестве примера давайте рассмотри пример конечного автомата, эмитирующего работу двери с кодовым замком:
Состояниями указанного автомата будут являться:
1. Начальное состояние
2. Дверь закрыта, замок защелкнут
3. Замок проверяет код
4. Дверь закрыта, замок открыт
5. Дверь открыта
События, которые должен принимать автомат, будут следующие:
1. Набор кода
2. Код введен правильно
3. Код введен не правильно
4. Открывание двери
5. Закрывание двери
Схематически данный автомат можно изобразить следующим образом:


Добавив 4 состояния в конечный автомата (начальное состояние добавлено автоматически), получим следующий автомат:

Обычно состояние конечного автомата состоит из трех программных компонентов:
1. StateInitializationActivity – выполняет операции при переходе автомата в состояние.
2. Один или несколько EventDrivenActivity – отвечают за перехват сообщений из внешнего мира
3. StateFinalizationActivity – выполняет операции перед тем как автомат покинет текущее состояние.
Ни один из указанных компонентов не является обязательным.
Для начала поместим в Workflow1InitialState два компонента: StateInitializationActivity и StateFinalizationActivity (с компонентом EventDrivenActivity мы познакомимся в следующий раз).
В результате получим:

Двойной клик на любой Activity добавленной в состояние открывает линейный workflow в котором задаются действия выполняемые при передачи управления данному Activity.

Добавим компонент CodeActivity, который позволяет выполнить закрепленный за ним обработчик. Добавим в него код вида:
private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Дверь создана и готова к работе");
}


* This source code was highlighted with Source Code Highlighter.


Для перехода в другое состояние используется SetStateActivity, состояние в которое необходимо перейти в данном Activity задается свойством: TargetStateName. В нашем случае его необходимо задать в CloseClose:


Добавим в состояние CloseClose StateInitializationActivity аналогичный Workflow1InitialState указав в качестве выполняемого кода следующий метод (в класс Workflow1 также добавляем строковое поле класса string):
string code = "";

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Введите код:");
code = Console.ReadLine();
}


* This source code was highlighted with Source Code Highlighter.
У SetStateAtivity укажем в качестве TargetStateName состояние TestKey.
В состоянии TestKey в StateInitializationActivity помести проверку ключа на равенство «111». Для этого воспользуемся компонентом IfElseActivity. Все ветви кроме одной данного Activity должны иметь проинициализированным свойство Conditon. В неашем примере установим значение этого свойства в «Declarative Rule Condition» (т.е. само Activity проверяет некое декларативное правило задаваемое при помощи мастера доступного в свойстве ConditionName. Для провекри на соответствие введенного кода введм следующее правило: this.code == "111".
После чего в левую ветвь поместим SetStateActivity с переходом на CloseOpen, а в правую с переходом на CloseClose.

Я думаю, остальные состояния трудности при написании не вызовут. Попробуйте написать их самостоятельно. В результате должно получится что то похожее:

Запускаем и проверяем работу конечного автомата.
Для ленивых работоспособный проект можно скачать здесь.

вторник, 19 мая 2009 г.

Разработка выпадающего меню: примеры и лучшая практика

http://blog.yed-prior.com/archives/47

Многопоточный доступ в WPF

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

Для примера возьмем простое приложение, со следующим интерфейсом:

При нажатии на кнопку "Старт" запускается длительная операция выводящая состояние процесса в ProgressBar.


Код метода длительной операции для простоты возьмем вот такой:

private int LongOperation()
{
for (int i = 0; i < 100; i++)
{
// Имитация полезной работы
Thread.Sleep(100);
// Сообщаем в визуальную часть, что часть работы выполнена
SetProgressBarValue(i + 1);
}
return 0;
}


* This source code was highlighted with Source Code Highlighter.


Обработчик клика на кнопке:


private void startButton_Click(object sender, RoutedEventArgs e)
{
Func<int> operation = LongOperation;
operation.BeginInvoke(null, null);
}


* This source code was highlighted with Source Code Highlighter.

При попытке реализовать метод SetProgressBarValue в лоб:


private void SetProgressBarValue(int newValue)
{
workProgress.Value++;
}


* This source code was highlighted with Source Code Highlighter.

мы получим InvalidOperationException

Для решения этой проблемы необходимо воспользоваться замечательным классом Dispatcher, переписав с его помощью код, мы получим:


// Делегат для перевызова метода SetProgressBarValue через диспетчер
private delegate void SetProgressBarValueHandler(int newValue);

private void SetProgressBarValue(int newValue)
{
// Проверяем совпадает ли поток диспетчера с потоком вызвавшим метод
if (Dispatcher.Thread == Thread.CurrentThread)
{
// Все замечательно :) меняем значение прогресбара
workProgress.Value = newValue;
}
else
{
// Нет :( все плохо :( перезапускаем метод в потоке диспетчера
Dispatcher.Invoke(new SetProgressBarValueHandler(SetProgressBarValue), new object[] { newValue });
}
}


* This source code was highlighted with Source Code Highlighter.

Вуаля, все заработало :)

Проект можно скачать здесь: MultithreadingWPF.rar