気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
WPFではタスクトレイ(システムトレイ)にアイコンを表示させるためのコンポーネントが提供されていません。
でも、ご心配なく。System.Windows.FormsのNotifyIconを使う方法があります。

タスクトレイにアイコンを表示し、一度、WPFウィンドウを閉じても、タスクトレイアイコンを
クリックすることで再表示させるアプリケーションを作ってみました。

まず、WPFアプリケーションのメインウィンドウクラスにSystem.Windows.FormsのNotifyIconのインスタンス変数を宣言します。

    public partial class MainWindow : Window
    {
        private System.Windows.Forms.NotifyIcon _notifyIcon;
///省略

そしてメインウィンドウのコンストラクタでNotifyIconを初期化し、タスクトレイに表示されるようにします。

    public MainWindow()
    {
        InitializeComponent();

        //タスクバーに表示されないようにする
        ShowInTaskbar = false;

        //タスクトレイアイコンを初期化する
        _notifyIcon = new NotifyIcon();
        _notifyIcon.Text = "タスクトレイサンプル";
        _notifyIcon.Icon = new System.Drawing.Icon("app.ico");

         //タスクトレイに表示する
        _notifyIcon.Visible = true;

         //アイコンにコンテキストメニュー「終了」を追加する
         ContextMenuStrip menuStrip = new ContextMenuStrip();

         ToolStripMenuItem exitItem = new ToolStripMenuItem();
         exitItem.Text = "終了";
         menuStrip.Items.Add(exitItem);
         exitItem.Click += new EventHandler(exitItem_Click);

         _notifyIcon.ContextMenuStrip = menuStrip;

         //タスクトレイアイコンのクリックイベントハンドラを登録する
         _notifyIcon.MouseClick += new System.Windows.Forms.MouseEventHandler(_notifyIcon_MouseClick);
    }

NotifyIconのIconに設定するアイコン画像は、プロジェクトに追加し、プロパティを修正して
ビルドアクションを「なし」に、「出力ディレクトリにコピー」を「新しい場合はコピーする」に設定しました。

こうすることでICONファイルがアセンブリに埋め込まれず、ビルド時に、exeファイルと同じディレクトリに
出力されるようになります。そして、System.Drawing.Iconクラスのコンストラクタには、exeファイルからの
相対パスを指定しています。

リソースとして埋め込むことができない(と思います)ので、こうしました。

そして、タスクトレイのアイコンに、コンテキストメニューを設定し、アプリケーションを終了させるための
「終了」メニューを追加しています。

次にウィンドウが閉じられても、アプリケーションが終了しないように、ウィンドウのClosingイベントを処理します。

    //ウィンドウが閉じられる前に発生するイベントのハンドラ
    private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        try
        {
            //閉じるのをキャンセルする
            e.Cancel = true;

            //ウィンドウを非可視にする
            Visibility = System.Windows.Visibility.Collapsed;
        }
        catch { }
    }

実際には閉じられる際に、閉じる処理をキャンセルし、ウィンドウを見えなくするようにしています。

そして、NotifyIconがクリックされると、ウィンドウを再表示するようにしています。

    //NotifyIconのクリックイベントのハンドラ
    private void _notifyIcon_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e)
    {
        try
        {
            if (e.Button == MouseButtons.Left)
            {
                //ウィンドウを可視化
                Visibility = System.Windows.Visibility.Visible;
                WindowState = System.Windows.WindowState.Normal;
            }
        }
        catch { }
    }

NotifyIconに設定したコンテキストメニューの終了メニューがクリックされるとアプリケーションを終了させます。

    //終了メニューのイベントハンドラ
    private void exitItem_Click(object sender, EventArgs e)
    {
        try
        {
            _notifyIcon.Dispose();
            System.Windows.Application.Current.Shutdown();
        }
        catch { }
    }

意外と簡単にできました。

それにしても何故、WPFで提供しないのか不思議です。何度もリクエストが挙っているみたいですが、
「WPFはあくまでもプレゼンテーションライブラリなので」という理由で却下されているようです。。。。

スポンサーサイト
WPFのComboBoxでは「TextChanged」イベントが提供されていません。これが結構不便な時があります。
TextInputイベントだと「Backspace」や「Space」が拾われません。KeyDownイベントだと、
拾えるのですが、自分でTextに対して処理をしなければいけません。これは面倒くさいです。

そこでComboBox内のTextBoxにアクセスするためのテクニックをご紹介します。

まず、ComboBoxを拡張したカスタムコントロールを作成します。コントロールのスタイルに関しては
ComboBoxのものをそのまま使いたいので、以下のようにします。

   static ConditionComboBox()
   {
       //ComboBoxの見栄えを採用する
       DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomComboBox), 
              new FrameworkPropertyMetadata(typeof(ComboBox)));
   }

最初はGeneric.xamlに定義するカスタムコントロール用のスタイルを使用するように指定されているのですが、
これをComboBoxのスタイルを使用するように変更します。(黄色字の部分)

次にOnRenderメソッドをオーバーライドして次のように記述します。

   protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
   {
       base.OnRender(drawingContext);

       //ComboBox内のTextBoxを取得する
       TextBox textBox = this.GetTemplateChild("PART_EditableTextBox") as TextBox;

       //TextChangedイベントにハンドラを登録する
       textBox.TextChanged += new TextChangedEventHandler(textBox_TextChanged);
   }

この方法は、TextBoxに直接アクセスできるので、TextChangedイベントを処理する他に、
TextBoxの挙動を変えたい場合などにも使えます。

WPFのListViewを初めて使用する方は、WindowsフォームのListViewでいう「LargeIconモード」が、WPFのListViewでは提供されていないことに
驚きとショックを受けるのではないでしょうか?

私もショックを受けた1人です。先日、発表させていただいた英会話学習ソフト「イメージで覚える英単語」では、
LargeIcon風のデザインで、英単語と英単語に対応した画像を表示しています。

(画面ショット)


これをやるために、ListViewを使おうとしたのですが、WPFのListViewにある表示モードでは、それが出来ないことが分かり愕然としました。
一時はListViewをあきらめて、WrapPanelで実装しようかと思っていたのですが、DataBindingや項目選択の機能など、
欠かせない機能があるため、ListViewを拡張する決心をしました。

ネット上のリソースをいろいろ探したところ、いくつかやり方があるみたいでしたが、以下のサイトのやり方を
参考に、実装しました。

How to Create a Custom View

ざっくり言うと、やることは以下の3つです。
1. 新しい表示モードが使用するスタイルを定義する。
2. ListViewの表示モードの基底クラスであるViewBaseを拡張し、新しい表示モードを作成する。
3. ListViewの表示モード(Viewプロパティ)に作成した表示モードを指定する。

まず、1番目のスタイル定義から説明します。
「Themes」ディレクトリ以下に、「Generic.xaml」を作成し、以下のようにして、
「ListView」と「ListViewItem」のスタイルを定義しまます。

<ResourceDictionary xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
                    xmlns:l="clr-namespace:ImageWord.Controls">   
    <!-- ListViewのスタイル定義 -->
    <Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type l:ImageView},
                                               ResourceId=ImageView}" ・・・・①
        TargetType="{x:Type ListView}" BasedOn="{StaticResource {x:Type ListBox}}">
        <Setter Property="BorderBrush" Value="Black"/>
        <Setter Property="BorderThickness" Value="0.5"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Border Name="bd" BorderBrush="{TemplateBinding BorderBrush}" 
                           BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}" Margin="{TemplateBinding Margin}">
                        <ScrollViewer Margin="{TemplateBinding Padding}">
                            <WrapPanel ItemWidth="160" IsItemsHost="True" MinWidth="100"
                              Width="{Binding ActualWidth,
                                RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}">
                            </WrapPanel>
                        </ScrollViewer>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- ListViewItemのスタイル定義 -->
    <Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type l:ImageView},
                                        ResourceId=ImageViewItem}" ・・・・②
         TargetType='{x:Type ListViewItem}' BasedOn='{StaticResource {x:Type ListBoxItem}}'>
        <Style.Resources>
            <l:UriImageConverter x:Key="uriImageConverter"/>
        </Style.Resources>
        <Setter Property='Padding' Value='3'/>
        <Setter Property='Margin' Value='5'/>
        <Setter Property='HorizontalContentAlignment' Value='Center'/>
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Border Background="White">
                        <StackPanel Orientation="Vertical">
                            <Grid Width="150" Height="126">
                                <Image Margin="0,3,0,3" Source="{Binding ImageFullPath}" ・・・・③
                                      Width="{Binding PreferredWidth}" Height="{Binding PreferredHeight}"/>
                             </Grid>
                            <TextBlock Text="{Binding Text}" TextWrapping="Wrap" HorizontalAlignment="Center" 
                                       FontSize="15" Name="WordText" Foreground="Black" Margin="0,0,0,5"/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

設定したスタイルの内容の詳細は省略させて頂きますが、ポイントの1つ目は、それぞれ「ListView」と「ListViewItem」を
TargetTypeとするスタイルを定義することです。

そして、2つ目のポイントは、それぞれのスタイルの「x:key」にComponentResourceKeyを指定することです。(上記①と②)
なぜ、「ComponentResourceKey」で無ければならないか・・・・すみません、分かりません。真似しました。
(サンプルをいくつか見ましたが、みんなこうしていました。理由が分かる方、教えてくださいませ。)
ここに指定したキーを後で、ViewBaseを拡張したクラスの中で使用します。

③の部分で、ListViewItemのContentTemplateの中にImageを定義していますが、SourceのBindingに指定している
「ImageFullPath」はListViewにバインドしたコレクション(ObservableCollection)に格納されている要素の
属性です。
(ちょっと分かりにくいですね。。OberservableCollectionにWordというクラスを詰めてそれをListViewにバインドしています。
そして、WordクラスのプロパティとしてImageFullPathが定義されていて、そこに画像のフルパスが設定されています。)

2つめの手順はViewBaseの拡張です。

namespace ImageWord.Controls
{
    public class ImageView : ViewBase
    {
        //ListViewのスタイルのキーを返す
        protected override object DefaultStyleKey
        {
            get { return new ComponentResourceKey(GetType(), "ImageView"); }
        }

        //ListViewItemのスタイルのキーを返す
        protected override object ItemContainerDefaultStyleKey
        {
            get { return new ComponentResourceKey(GetType(), "ImageViewItem"); }
        }
    }
}

といってもやることは少しです。ViewBaseのプロパティ「DefaultStyleKey」と「ItemContainerDefaultStyleKey」
をオーバーライドして「Genaric.xaml」でListViewとListViewItem用に定義したスタイルに割り当てたキーを
返却するだけです。

「Genaric.xaml」ではこんな風にキーを定義していました。

x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type l:ImageView},ResourceId=ImageView}"

TypeInTargetAssemblyにImageViewのTypeインスタンスをResourceIdにImagwViewという文字列を指定しています。
ViewBaseで返却しているComponentResourceKeyにも同じ指定をしていることが分かると思います。

最後の3番目の手順ですが、XAMLでListViewのViewに作成したImageViewクラスを指定します。

     <ListView>
         <ListView.View>
             <l:ImageView/>
         </ListView.View>
     </ListView>

後はListViewのItemsSourceにデータソースになるコレクションを設定するのみです。

今日は、URLで指定された画像をWPFで表示する方法を紹介します。
画像の表示には「Image」クラスを使用します。

早速、コードを見てください。

   Image image = new Image();
   BitmapImage imageSource =  new BitmapImage(new Uri(画像のURL));
   image.Source = imageSource;

上記のサンプルでは、ImageクラスのインスタンスをC#側で生成していますが、
XAMLで宣言しても同じです。

URLの画像の取得には「BitmapImage」クラスを使用します。
画像の指定方法はコンストラクタにUriを指定するだけです。

そして、そのBitmapImageのインスタンスをImageのSourceプロパティに設定します。
画像のダウンロードは非同期で行われ、完了次第画面に表示されます。

これだけでは一点問題があります。

Imageのサイズが自動的に調整され、大きくなったりしていまいます。
これを防ぐためには以下のようにします。

   Image image = new Image();
   BitmapImage imageSource =  new BitmapImage(new Uri(画像のURL));
   image.Source = imageSource;

   //画像のロード完了イベントを処理して、画像のサイズを設定する
   imageSource.DownloadCompleted += new EventHandler((Object sender,EventArgs e) =>
   {
       image.Width = imageSource.PixelWidth;
       image.Height = imageSource.PixelHeight;
   });


BitmapImageクラスのPixelWidth、PixelHeightプロパティを使用すれば良いのですが、
先ほど、言いましたように画像のダウンロードは非同期で行われるため、
Sourceを設定した直後に取得しても0が返されてしまいます。

そこで上記のコードのように、BitmapImageクラスのDownloadCompletedイベントを処理して、
画像のロード完了後に画像のサイズを取得するようにします。
(ハンドラはラムダ式の匿名メソッドにしました。)

以前の記事で、SilverlightでActualHeightやActualHeightを取得する方法を紹介しました。
Silverlightで発生するActualHeightやActualWidthを取得できない現象は同じくWPFでも発生します。

対処方法や考え方はSilverlightと同じなのですが、コードレベルでは少し異なる部分もあるので、
あらためて記事にしたいと思います。

まず、この現象が発生する原因については、Silverlight版の記事を参照してください。

以下は現在私が開発しているソフトのMainWindowのコンストラクタです。
InitializeComponent()でコントロールが初期化された後に各コンポーネントのサイズを
調整する処理を記述しています。

        public MainWindow()
        {
            InitializeComponent();

            //遅延処理を行う
            Dispatcher.BeginInvoke(new Action(() =>
                {
                    //DockPanelのサイズを設定する
                    _rootPanel.Width = _rootCanvas.ActualWidth;
                    _rootPanel.Height = _rootCanvas.ActualHeight;

                    //ブラウザコントロールのサイズを設定する
                    _browser.Height = _rootCanvas.ActualHeight / 2;
                })
            ,DispatcherPriority.Loaded);
        }

Silverlight版と異なる点が2つあります。

1つ目は、BeginInvokeの第一引数の記述方法です。汎用デリゲートActionのコンストラクタにラムダ式の匿名メソッドを
指定して初期化しています。Silverlight版では、第一引数に直接匿名メソッドを記述するだけでもOKでした。

2つ目は、DispatcherPriorityを指定している点です。Silverlight版のBeginInvoke()では、DispatcherPriorityは指定
することが出来ませんでした。WPFでは逆にこれを正しく指定しないとActualHeightやActualWidthを取得することが出来ません。

レイアウトシステムの処理は指定できる優先順位の中で3つ目に高いRenderが割り当てられています。
そのため、Actualサイズを取得する処理にはこれより低い優先順位を指定しないといけません。

以上です。
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。