Last Updated 2011/09/21
私が WPF を研究する過程で、疑問に思ったことや理解が困難だった点とその解決法などについて解説します。解説する項目の順番には特に意味はなく、書くべきことがまとまった時点で加筆していくことにしました。同じような悩みを持つ人たちのお役に立つと思います。
WPF ではコントロールと呼ぶより、要素と呼ぶことが多くなります。これは UI 部を XAML で書くことに起因します。本サイトでは、前後の文脈によって、コントロールと要素とを使い分けています。
WPF に関係するクラスのクラス名全般に言えることですが、.Net Framework 2.0 までのクラス名の付け方とは明らかに異なります。ハッキリ言って、分かりにくくなりました。Visual クラスとくればどんな機能を思い浮かべますか。間違いなく、グラフィックス関係だろうと思いますよね。しかし、実態はユーザーインターフェース要素、つまり、ビジュアルコントロールの基本クラスです。BitmapEffect クラスはどうですか。ビットマップに対する視覚効果だと思ったら、ビットマップ(.bmp ファイル)に限らず、すべての画像形式を対象とする視覚効果を担当します。
さて、論理ツリーとビジュアルツリーとの概念は WPF ではきわめて重要です。しかし、「論理」という言葉はそれを思いついた人にしか理解できないことが多いものです。つまり、何を「論理」と呼ぶかは人によって異なるからです。論理ツリーとビジュアルツリーもその例にもれません。ちなみに、ビジュアルツリーのほうは Visual コントロールを継承する要素(コントロール)の階層構造です。
さて、「開発ツール」で紹介する Snoop というツールを使うと、WPF アプリケーションを構成するツリー構造を見ることができます。図は、Window コントロール(フォーム)に Grid コントロール を配置し、その中に ComboBox コントロールを 1 つ配置しただけのアプリケーションを構成する要素を階層化してあらわしたツリー構造です。
Window、Grid、ComboBox が論理要素、そのほかがビジュアル要素ですね。構成要素が少ないので、ツリーというほどのこともありませんが、WPF アプリケーションは多くのビジュアル要素で構成されていることがよく分かります。
ここまではずいぶんケチを付けましたが、その理由を説明します。Window、Grid、ComboBox を論理要素といいましたが、これらも Visual クラスを継承しますので、ビジュアル要素でもあります。つまり、左図の Application と ColumnDefinition クラス以外はすべて Visual クラスから派生するクラスです(ただし、両方ともコントロールとはいえませんが)。
要するに、WPF の仕様を考えた人はユーザーが宣言、つまり、明示的に追加した要素を論理要素と呼んでいるのではないかと想像します。
逆に言うと、WPF のコントロールは 1 つのコントロールであっても、多くの要素で構成されていることが分かります。このことはルーティングイベントの項にも深く関係していますので、覚えておいてください。
ルーティングイベント "routing event" は従来の .Net Framework にはなかった概念です。"routing" は「道筋をたどる」でいいと思いますが、前項のツリーの話を思い出してください。道筋とはこのツリーをさしています。
さて、下図は、イメージとテキストを表示するボタンコントロールです。Button コントロールに StackPanel コントロールを配置し、その内部に Image コントロールと TextBlock コントロールを配置しました。
この部分を先に紹介した Snoop を使ってビジュアルツリーを見ると、下図のようになります。
ボタンは Button コントロールだけでなく、多くの要素で構成されていることはすでに説明しました。アプリケーションを起動し、ユーザーがボタンをクリックすると、ボタンの Click イベントの発生を受けて何らかの処理をするわけですが、Image コントロール部分をクリックすると、Button コントロールの Click イベントは発生しないことになります。つまり、Image コントロールをクリックしても Button コントロールの Click イベントを発生させなければなりません。これを解決するための仕組みがルーティングイベントです。
ここまでの説明でルーティングの意味が見えてきましたね。ルーティングとはツリーをたどって本来イベントを処理すべきところまで(イベントハンドラの定義があるところ)たどり着くことです。
ルーティングはほとんどの場合、下位の要素から上位の方向にさかのぼります。この状態を泡がのぼっていく様子をもじってバブル型(bubble)のルーティングと呼びます。逆に、上位の要素から下位の方向に下ってくるルーティングをトンネル型(tunnel)と呼び、親要素のほうでイベントを処理するチャンスを持つことができます。たとえば、ここでイベントを処理済みと設定すると子要素のイベントは発生しないようにできます。トンネル型のイベントの名前には "Preview" が付いていますからすぐに分かります。最後に、直接型(direct)のルーティングもあります。これは .Net Framework の通常のイベントと同じで、イベントを発生した要素がイベントを処理します。
ファンクションキーはボタン一発で処理を実行できるので、便利な機能です。最近はマウス操作を好む人が多くなりましたが、ファンクションキーに慣れるとツールボタンをマウスでクリックすることに冗長さを感じます。そこで、WPF アプリケーションでファンクションキーを使う手順を説明します。なお、この項は前項(ルーティングイベント)の続きとして設けました。
ファンクションキーは、フォームにフォーカスがあるときにキーボードのキーを押すと発生するイベントをとらえて処理します。
次のコードは、フォームレベルでキー入力をとらえるため、PreviewKeyDown イベントを設定しています。
<Window x:Class="Test.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" PreviewKeyDown="Window_PreviewKeyDown"> ← 追加 .... </Window>
以下は、PreviewKeyDown イベントハンドラです。
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { Title = e.Key.ToString(); }
アプリケーションを起動し、適当なキーを入力してみてください。たとえば、[F5] を押すと、フォームのタイトルに "F5" と表示されます。ファクションキーを処理するときの実務的なコードは、switch ステートメントでキー入力を振り分けることになるでしょう。
プロパティ値の継承とは、ローカル値またはスタイルを通じて、あるプロパティ値が割り当てられていない子要素がある場合、それらの子要素に問題のプロパティ値の設定がある最も近い親要素の値を割り当てることです。
データバインディングの例として、「WPF とは何か」のページの「動的なデータドリブン型アプリケーションの構築」の項で、スライダコントロールのプロパティ値とバインディングする例を取り上げました。これはもっとも簡単な例ですが、データバインディングはもっと複雑なことも可能です。
しかし、WPF のデータバインディングはこれだけで 1 冊の本が書けるほど面倒です。いずれ詳しく解説する予定ですが、ここではデータバインディングの重要性と可能性を感じてもらえば十分なので、もう一つの例を挙げることにとどめたいと思います。
<Window x:Class="ComboBoxTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:src="clr-namespace:ComboBoxTest" Title="Window1" Height="300" Width="300" Loaded="window1_Loaded" > // Window のリソースに追加 <Window.Resources> <src:Cities x:Key="Cities"/> </Window.Resources> <Grid> <ComboBox Name="comboBox1" Width="200" Height="25" ItemsSource="{Binding Source={StaticResource Cities}}" IsSynchronizedWithCurrentItem="True"> // 最初の項目を選択する </ComboBox> </Grid> </Window>
ComboBox コントロールの ItemsSource プロパティ値は ObservableCollection オブジェクトから派生するクラスとして作成します。
using System.Collections.ObjectModel; // ObservableCollection namespace ComboBoxTest { public partial class Window1 : Window { public Window1() { InitializeComponent(); } } //****************************************************** public class Cities : ObservableCollection<string> { public Cities() { Add("札幌市"); Add("仙台市"); Add("静岡市"); Add("名古屋市"); Add("神戸市"); Add("福岡市"); } } }
コントロールのスタイルとは、コントロールの形状をのぞく外観と考えてください。たとえば、背景色やフォントなどです。コントロールの形状、たとえば、ボタンコントロールを円形にするにはコントロールテンプレートを使わざるを得ません。ただし、次の項で説明しますが、テンプレートを使うには多くのコードを書かなければなりません。
ともあれ、ボタンコントロールのスタイルを変更するコード例を示します。
コントロールのスタイルを変更するにはズバリ Style クラスを使います。このクラスの使い方はいろいろあると思いますが、もっとも一般的と思われる手順(模範的かどうかは分かりませんが)について説明します。ポイントは Style クラスの BasedOn プロパティです。このプロパティにはスタイルの元となるクラスを指定します。伝統的な Windows プログラミングにおけるサブクラス化と似ていますね。元のスタイルのうち、変更したいプロパティだけを設定します。
以下のコード例では、単純にフォントのスタイルを変更する例と、フォントの色付けとして線形グラデーションブラシを使う例を取り上げました。
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Window.Resources> <LinearGradientBrush x:Key="ButtonBrush" StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Red" Offset="0.0" /> <GradientStop Color="OrangeRed" Offset="0.25" /> <GradientStop Color="Blue" Offset="0.70" /> <GradientStop Color="Green" Offset="1.0" /> </LinearGradientBrush> <Style x:Key="ButtonStyle1" BasedOn="{StaticResource {x:Type Button}}" TargetType="Button"> <Setter Property="Foreground" Value="DodgerBlue" /> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="20" /> <Setter Property="FontStyle" Value="Italic" /> <Setter Property="FontWeight" Value="UltraBold" /> </Style> <Style x:Key="ButtonStyle2" BasedOn="{StaticResource {x:Type Button}}" TargetType="Button"> <Setter Property="Foreground" Value="{StaticResource ButtonBrush}" /> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="20" /> <Setter Property="FontStyle" Value="Italic" /> <Setter Property="FontWeight" Value="UltraBold" /> </Style> </Window.Resources> <Grid> <Button Height="29" Margin="64,34,98,0" Name="button1" VerticalAlignment="Top" Style="{StaticResource ButtonStyle1}">Button1</Button> <Button Height="29" Margin="64,78,98,0" Name="button2" VerticalAlignment="Top" Style="{StaticResource ButtonStyle2}">Button2</Button> </Grid> </Window>
コントロールテンプレートのコード例が以下に示す項目の中にあります。
.NET の開発 . .Net Framework SDK ... .Net Framework ..... Windows Presentation Foundation ....... コントロール ......... コントロールのカスタマイズ ........... ControlTemplate の例
コードは、「コントロールのカスタマイズ」-「ControlTemplate を使用したスタイル設定のサンプル」のページの中にあるサンプルファイルをダウンロードしてください(ダウンロードといっても WEB サイトからではなく、Visual Studio の中にあります)。このプロジェクトを見れば、WPF アプリケーション用コントロールがどのように作られているかが分かります。コントロールの色とスタイルは異なりますが、標準のコントロールも同様のテンプレートを使っているはずです。
コードを見ると、コントロールがフォーカスを受けたときの形状やいろいろなイベントに対応していることが分かります。つまり、テンプレートを使う場合は、コントロールにかかわるすべての機能を実装しなければなりません。たいていの場合はスタイルの変更程度で済むのではないかと思いますので、それで間に合うのであれば、スタイルの変更だけで我慢することをおすすめします。
さて、コントロールテンプレートのよいコード例がないかと、WEB サイトを調べましたが、完結しているものは結局ありませんでした。どこかに手抜きがあって実用性がありません。なければ作るが私の信条ですから作ってみました。下図を見てのとおり、楕円形のボタンコントロールです(縦横のサイズを同じにすれば円形になります)。
標準のボタンがそうであるように、マウスポインタをボタン上に移動したとき(ホバリングですね)、クリックしたとき、フォーカスがあるとき、などの状態に対応して外観を変化させています。色は標準のもの(ブルー系のグラデーション)と同じにしました。プロジェクトを公開しますので、試してください。プロジェクトは Visual Studio 2008 対応です。
ButtonText.lzh (51,327 bytes)
伝統的な Windows プログラミングにおいて、グラフィックス関係の測定単位はピクセルです。また、フォントのサイズは通常ポイント数で設定します。しかし、WPF では大きく異なります。というのは、WPF では環境に依存しないアプリケーション開発を目的としているからです。
さて、ピクセル単位は相対的な単位なので、解像度が異なる環境では元の状態を維持することができません。たとえば、1024 x 768 のディスプレイ上でデザインしたフォームを 1280 x 1024 のディスプレイで表示すると小さく表示されることは経験的に理解できると思います。
かつて、Visual Basic が登場したとき、Twips という単位が導入されました。これは "Twentieth of a Point" の略で、ポイントの 1/20 を意味します。1 ポイントは 1/72 インチですから 1 Twips は 1/1440 インチということになります。つまり、Twips は絶対的な単位です。
ポイントは元々印刷業界における単位で、厳密には 1/72 インチではありませんが、Windows では 1/72 インチとしています。
しかし、Windows API 関数がピクセル単位であることに原因があるのかもしれませんが、Twips が普及することはなかったように思います。
WPF は見たままをどんな環境でも再現できることが基本思想ですから、ピクセルのような相対的な単位系ではなく、絶対的な単位系を採用することになりました。それが WPF SDK に頻繁に登場する「デバイスに依存しない単位(1 単位は 1/96 インチ)」です。
「デバイスに依存しない単位」についてはここまでの説明で理解してもらえると思いますが、問題は「1 単位は 1/96 インチ」のほうです。なぜ、1/96 インチなのかですね。
ここで、Windows Vista のコントロールパネルの [個人設定] を開いて、画面左上の [タスク] を見ると、「フォントサイズ(DPI) の調整」があります。ここを選択すると、[既定のスケール(96 DPI)] にチェックが入っていると思います。これは、現在のディスプレイが 96 DPI であることを示しています。DPI は "Dots Per Inch" の略で、"Pixels Per Inch" と意味は同じです。つまり、「1 単位は 1/96 インチ」は 96 DPI の環境ではピクセル単位と同じになります。ただし、絶対的な単位ですから、異なる環境にもっていっても同じ位置とサイズを維持することになります。
さて、コントロールやグラフィックスの位置やサイズを指定するプロパティの説明の中に以下に示すような記述があります。
単位指定子 px、in、cm、pt のいずれかが後に続く、上に挙げた double 値 (Auto を除く)。 px (既定値) はデバイスに依存しない単位 (1 単位は 1/96 インチ) in はインチです (1in==96px)。 cm はセンチメートルです (1 cm==(96/2.54) px)。 pt はポイントです (1pt==(96/72) px)。
px は "pixel" を思い浮かべますが、決してピクセル単位ではありません。ただ、偶然にピクセルと一致することが多いというだけですので、勘違いしないようにしてください。論理ピクセルと理解しておいてください。
以下のコードを試してください。画面上のボタンの高さを物差しで実測すると 1 cm になるはずです。一方、Width プロパティのほうは単位を明示的に指定していないので、デフォルトの単位になります。
<Button Height="1cm" Width="75">button1</Button>
ところで、「デバイスに依存しない単位」に何か適当な名前を付けてくれれば便利なのですが、Microsoft はこういうことに気がきかない会社であることは残念なことではあります。
ある本の中に、「論理ピクセル」という表現を見つけました。Microsoft がこれを使うことにするといってくれれば決まりなのですが!
このサイトをご覧になった人からメールをいただきました。その人は 87 DPI のディスプレイを使っていて、上記のコードにしたがってディスプレイ上に表示されたボタンのサイズを実測すると、約 1.1 cm になるということです。これは 96 / 87 = 1.103 からの計算と一致します。つまり、「デバイスに依存しない単位」の前提がくずれています。ヒョットすると、実行環境が 96 DPI 未満の場合はスケーリングの手順を省略するのかもしれません。
ここまで書いて気付きましたが、アンチエイリアシングが関係しているのかもしれません。アンチエイリアシングとはたとえば、斜めの直線を描画するとき、本来の線のまわりにグレーなどの中間色のピクセルを配置し、直線を滑らかに見せようとするテクニックです。これが解像度の低すぎるディスプレイではうまくいかないので、96 DPI を下限としているのかもしれません。自信はありませんが。
どういうことなんでしょう。もし答えをお持ちの人がいましたら、連絡をいただけるとありがたいです。
なお、SystemParameters クラスの中に、以下に示すような internal static なメソッドがあります。
internal static double ConvertPixel(int pixel) { int dpi = Dpi; if (dpi != 0) { return ((pixel * 96.0) / ((double) dpi)); } return (double) pixel; }
SystemParameters クラスの PrimaryScreenWidth プロパティは Windows API 関数の GetSystemMetrics 関数を SM_CXSCREEN (= 0) を引数として呼び出したものですが、結果はピクセル単位で戻ります。これをデバイスに依存しない単位に変換するために使うメソッドです。pixel は変換するピクセル単位の数値、Dpi は現在のディスプレイの解像度ですね。Windows 環境のディスプレイの解像度は通常 96 DPI ですから、この場合はピクセル単位と同じになることが分かります。
ここで話を元に戻すと、このメソッドを使っていれば上記のような結果にはならないはずなのですが?
em サイズは元来印刷業界用語です。活字の高さが 9 ポイントの "M" の幅が 9 ポイントになるので、印刷業界では基準の測定単位になっているようです。しかし、コンピューター業界における em サイズに対する統一した定義はありません。活字を使わないので、活字を前提とする定義に意味はないからなのかもしれませんが。
では、WPF における em サイズとは何か。WPF SKD の中では「全角文字を基準とするサイズ」とあります。しかし、これでは何のことか。Microsoft は伝統的に用語の定義については気にしないところがあってユーザーはひどく混乱しますが、em サイズについてもその例外にもれません。そこで、実験的に確かめることにしましょう。
TextBlock コントロールの FontSize プロパティには「デバイスに依存しない単位 (1/96 インチ)」で指定するとあります。そこで、このプロパティに 48 を設定し、TextBlock コントロールの FontFamily プロパティに "Times New Roman" を指定します。この状態で以下のコードを実行してください。
private void button1_Click(object sender, RoutedEventArgs e) { Typeface typeFace = new Typeface("Times New Roman"); StringBuilder sb = new StringBuilder(); sb.AppendLine(String.Format("CapsHeight : {0}", typeFace.CapsHeight)); sb.AppendLine(String.Format("StrikethroughPosition : {0}", typeFace.StrikethroughPosition)); sb.AppendLine(String.Format("UnderlinePosition : {0}", typeFace.UnderlinePosition)); sb.AppendLine(String.Format("XHeight : {0}", typeFace.XHeight)); GlyphTypeface glyphTypeFace = new GlyphTypeface(); if (typeFace.TryGetGlyphTypeface(out glyphTypeFace) == true) { sb.AppendLine(String.Format("Baseline : {0}", glyphTypeFace.Baseline)); sb.AppendLine(String.Format("Height : {0}", glyphTypeFace.Height)); } sb.AppendLine(String.Format("LineSpacing : {0}", typeFace.FontFamily.LineSpacing)); textBox1.Text = sb.ToString(); }
実行結果: CapsHeight : 0.662109375 StrikethroughPosition : 0.2587890625 UnderlinePosition : -0.10888671875 XHeight : 0.447265625 Baseline : 0.89111328125 Height : 1.107421875 LineSpacing : 1.14990234375
TextBlock コントロールの Height プロパティに "Auto" を指定して、TextBlock コントロールの ActualHeight プロパティを調べると 55.2 になりますが、これは 48 x LineSpacing (= 1.14990234375) です。つまり、em サイズはデバイスに依存しない単位 (1/96 インチ) で指定したフォントのサイズということになります。
そのほかの数値もそれぞれ 48 を掛ければピクセル数として得られます。
下図はこの仮説(?)に基づいて上記の実行結果を図示したものです。ディスプレイ上で表示する場合は整数に丸められるのでピタリに合うわけではありませんが、実測するとおおむね一致します。
つまり、Microsoft は em サイズを基準のサイズという意味で使っているのではないかと思います。なお、XHeight の意味は、小文字の "x" のベースラインから上端までの高さがもっとも小さいことに由来するようです。
−以上−