ビットマップフォントをなんとなく縁取り
お仕事でそういう需要があったのです。
縁取り…アウトラインのデータが取れるならパスに沿ってラインを描くのがよさそうなんだけど、ビットマップだと…?
たぶん、縁取りって元の文字の線を太らせたものを下に敷くのでいいのでは?
問題はどうやって文字を太らせるか?単純に考えると線のピクセルを外側に押し出してやればよさそうですけども、シェーダーで描くときなんかは描画するピクセルにはどこから押し出されてくるのか?で考える必要があります。例えば下図の赤いピクセル。
基本的な考え方としては隣接あるいは近隣のピクセルを参照してどうにかしようということになります。こういう場合はN×Nピクセル分のフィルターをつくって処理するのが定番ですね。例えば2ピクセル太らせるなら上下左右2ピクセル+自分で5×5のフィルターになります。
このフィルターの中で一番グレースケールの値が大きいピクセルをとってくる、だと斜め方向に太りやすくなってしまうので形がガタガタになってしまいます。たぶん。
なのでまずは「描画するピクセルを中心とした円に含まれる各ピクセルの面積の割合をグレースケール値に掛ける」という方法を試してみました。面積の割合は、難しい計算をするのは大変なので「各ピクセルを4x4のサブピクセルに分割してフィルターの中心からの距離が円の半径以内のピクセルを数える」という素直な方法にしました。
角のピクセルは16のうち3つのサブピクセルが半径以内なので3/16=0.1875となります。思ったより完全に円に含まれるピクセルが多い…
このフィルターを用いた太らせフォントの各ピクセルの計算は「参照ピクセルのグレースケール値×フィルターの値」の最大値を採用する、となります。これをもともとの描画ピクセルのグレースケール値とアルファブレンディングします。
Color Blend(Bitmap bmp, int x, int y, float[,] filter, Color outline) { float pix = 0; for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { Color p = bmp.GetPixel(x + i - N / 2, y + j < N / 2); float f = p.R * filter[i, j]; if (p > pix) { pix = p; } } } float a = bmp.GetPixel(x, y).R / 255.0f; int r = (int)(a * 255 + (1 - a) * outline.R * pix); int g = (int)(a * 255 + (1 - a) * outline.G * pix); int b = (int)(a * 255 + (1 - a) * outline.B * pix); return Color.FromArgb(r, g, b); }
そんな処理を施した結果がこちら。
右側はフィルターを可視化したもの。そこそこいい感じになったと思いますが元のフォントと縁取りの境目部分がすこしガタガタしているのが気になります。これは元々のビットマップの時点でガタガタしてたのである程度は仕方ないです。もう一つ気になるのは文字の払いの部分が丸まってしまっているところ。これはまあるいフィルターで計算してるからですね。これらは今後の課題です。
スクリプトエディタの枠組みっぽいものができた
ちょっと進んでなんだかそれらしい感じの枠組みができました。まだ見た目だけです。
一番外側がDockPanel で左にスクリプト名のListBox, 中央に選択されたスクリプトが表示されます。
<DockPanel> <!-- パネルの左側にスクリプトリストを配置 --> <ListBox ItemsSource="{Binding ScriptList}" DockPanel.Dock="Left" x:Name="ScriptList"> <ListBox.ItemTemplate> <DataTemplate DataType="local:ScriptData"> <TextBlock Text="{Binding ScriptId}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <!-- パネルの中央に選択されたスクリプトを配置 --> <ContentControl ContentTemplate="{StaticResource ScriptData}" Content="{Binding SelectedItem, ElementName=ScriptList}"> </ContentControl> </DockPanel>
あまり凝ったことはしてないですが、ContentoControl 側のContent に"ScriptList" と名前を付けたListBox の選択アイテムをバインドしてみたところすんなり反映されました。
表示されるスクリプトデータはテキストリソースから読み取ってMainWindow のDataContext に設定したものをバインディングしています。
スクリプトデータの詳細は こちら にあります。ここでは割愛します。
選択されたスクリプトのContentControl の中身もDockPanel で上にスクリプトID と説明、中央にコマンドリスト(スクリプト本体)を配置してます。枠の部分をContentControl にするのが適切なのかどうかはちょっと不安があるところです。
<!-- スクリプトデータ --> <DataTemplate x:Key="ScriptData" DataType="local:ScriptData"> <DockPanel> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <Label Content="{Binding ScriptId}"></Label> <Label Content="{Binding Description}"></Label> </StackPanel> <ListBox ItemTemplate="{StaticResource Command}" ItemsSource="{Binding CommandList}" SelectionMode="Multiple"> </ListBox> </DockPanel> </DataTemplate>
ビュー部分はとりあえず整ったのでそろそろロジック部分に取りかかれそうな気がしてきました。
ItemsControl でリストの内容を表示する
独自スクリプトのエディタを作ろうと思ってます。スクリプトはだいたいこんな感じ。
コマンド1 パラメータ1,パラメータ2,パラメータ3 コマンド2 パラメータ1,パラメータ2,パラメータ3,パラメータ4 コマンド3 ...
パラメータの部分のUI はこんな見た目。パラメータの値の部分はクリックするとテキストボックスになります。
このデザインにしたいのは個人的な趣味の割合が大きいです。普段はラベル、編集時だけテキストボックスにするのは前回の
クリックしたら編集できるようになるラベルがつくれた - 見切り発車でやった方法です。
パラメータは複数あってリストになってます。これを横に並べて表示するにはItemControls を使用します。
<StackPanel> <!-- パラメータリストを表示するItemsControl --> <ItemsControl Template="{StaticResource ParamList}" ItemTemplate="{StaticResource Param}"> <local:ScriptParameter ParamName="パラメータ1" Value="にゃー" /> <local:ScriptParameter ParamName="パラメータ2" Value="にゃにゃー" /> <local:ScriptParameter ParamName="パラメータ3" Value="にゃにゃにゃー" /> </ItemsControl> </StackPanel>
アイテムを横に並べて表示するためにItemsControl にTemplate を指定してます。ItemTemplate は各パラメータ用の表示用です。
<!-- パラメータリストを水平に並べる --> <ControlTemplate x:Key="ParamList"> <StackPanel Orientation="Horizontal" IsItemsHost="True"></StackPanel> </ControlTemplate> <!-- パラメータリストの各項目 --> <DataTemplate x:Key="Param" DataType="local:ScriptParameter"> <StackPanel Orientation="Horizontal" Margin="3,2"> <TextBlock Text="{Binding ParamName}" /> <TextBlock Text=":" Margin="2,0" /> <TextBox Text="{Binding Value}"> <TextBox.Style> <Style TargetType="TextBox"> <Style.Triggers> <!-- 普段はラベルに偽装する --> <Trigger Property="IsFocused" Value="False"> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Cursor" Value="Arrow" /> <!-- ただのラベルではないことを示すためハイパーリンク風に 青字+アンダーラインで装飾する --> <Setter Property="TextDecorations" Value="Underline" /> <Setter Property="Foreground" Value="Blue" /> </Trigger> <!-- マウスオーバー時に太字にする --> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsFocused" Value="False" /> <Condition Property="IsMouseOver" Value="True" /> </MultiTrigger.Conditions> <Setter Property="FontWeight" Value="Bold" /> </MultiTrigger> </Style.Triggers> </Style> </TextBox.Style> </TextBox> </StackPanel> </DataTemplate>
ネストが深いのが気になりますがやりたいことはできてるのでとりあえずOK。
クリックしたら編集できるようになるラベルがつくれた
ずいぶんと遠回りをしてしまった気がします。
Label あるいはTextBlock をクリックしたらTextBox に置き換えることばかり考えてましたが、TextBox の見た目をTextBlock に偽装することでおおむね望み通りの表現ができることに気づきました。
<TextBox Text="にゃー"> <TextBox.Style> <Style TargetType="TextBox"> <Setter Property="VerticalAlignment" Value="Center" /> <Style.Triggers> <Trigger Property="IsFocused" Value="False"> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Foreground" Value="Blue" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="TextDecorations" Value="Underline" /> </Trigger> </Style.Triggers> </Style> </TextBox.Style> </TextBox>
こんな感じです。
クリックしたら編集できるようになるラベルがつくりたい
某社のゲームエンジンのGUIがWPFと聞いて何度か挫折したWPFに再チャレンジしてはやひと月以上が経過してしまいました。
C#をやる目的は仕事で使うツールの作成がメインなので実際に今欲しいツールを作りながら学びたいと思うのですけども、「パラメータ名:値」っていうラベルがあって「値」の部分をクリックするとテキストボックスに変化して入力を受け付けて編集が完了したらまた静的ラベルにもどる、って感じのGUIを作りたいのですがなかなか難航してます。
パラメータと値を持ってるデータをバインディングしたいのでデータテンプレートを使うのがよさそうな気がするんだけどもラベルをクリックしたときに何かしようと思ったらコントロールテンプレートでボタンをラベルに偽装するのが良いのか?