気楽なソフト工房

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



mykonos2008

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

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
前回は、UserControl内のGridに動的に1つの「TabControl」を配置するコードを紹介しました。
本日は、2つの「TabControl」を左右に並べて配置する部分を紹介します。

通常モードの場合は、1行1列のGridに、1つのTabControlを配置していましたが、分割モードでは
Gridを1行2列にし、各列にTabControlを貼り付けます。以下がそれを行っている部分のコードです。

「TabHost.xaml.cs」
 public void OpenDouble()
 {

  解説:2つのTabControlをホストするGridの行と列を一旦
  クリアし、1行2列のGridになるように行と列の定義を追加し直します。

     //一旦クリアする
     _tabPanel.Children.Clear();
     _tabPanel.ColumnDefinitions.Clear();
     _tabPanel.RowDefinitions.Clear();

     //列を定義する
     ColumnDefinition col1 = new ColumnDefinition();
     _tabPanel.ColumnDefinitions.Add(col1);

     ColumnDefinition col2 = new ColumnDefinition();
     _tabPanel.ColumnDefinitions.Add(col2);

     //行を定義する
     RowDefinition row = new RowDefinition();
     _tabPanel.RowDefinitions.Add(row);

  解説:元々のTabControlに追加されていたTabページのうち、
  選択されていたページを右側のTabControlに移動させます。

     //現在選択されているTabのページを
     //右側に開くTabControlに移動する
     ExtendedTabItem selectedItem = _leftTab.SelectedItem as ExtendedTabItem;
     _leftTab.Items.Remove(selectedItem);

     _rightTab.Items.Add(selectedItem);

  解説:以前にも一度解説しましたがTabControlに
  Tabページが初めて追加された場合に内容が表示されないという現象があり
  それを回避するためにダミーのページを追加し、削除する処理を行っています。

     ExtendedTabItem item = new ExtendedTabItem();
     item.Header = "ダミー";
     item.Content = "ダミー";
     _rightTab.Items.Add(item);
     _rightTab.Items.Remove(item);

  最後にTabControlをGridに追加し、Grid上の位置を指定しています。

     //追加しなおす
     _tabPanel.Children.Add(_leftTab);
     Grid.SetColumn(_leftTab, 0);
     Grid.SetRow(_leftTab, 0);

     _tabPanel.Children.Add(_rightTab);
     Grid.SetColumn(_rightTab, 1);
     Grid.SetRow(_rightTab, 0);

     _isDivided = true;

  分割の操作を指示できるかどうかを示すフラグを
  falseに設定する(既に分割されているので)

     IsDividable = false;
 }

WPFのGridは柔軟性が有って、とても便利ですね。いろいろな場面で使えそうです。

次回は左右に並べたTabControlの幅を境界をドラッグすることにより、変更することが出来るように
しましたので、その方法をレポートします。

本日のソース




スポンサーサイト
現在、WPFで開発している仕訳入力用ソフト「Paella」は、以下の3つ画面を持つアプリケーションです。

・振替伝票入力
・振替伝票参照
・残高試算表

その中で、「振替伝票入力」と「振替伝票参照」は用途を考えると同時に参照したいページになると思います。
そこで、「Paella」では、ウィンドウ内に1つのTabControlを配置し、その中に各ページを置く通常モードと
ウィンドウ内に2つのTabControlを左右に並べて配置する分割モードをサポートするようにします。
以下が通常モードの画面イメージになります。

「通常モード画面」

この状態で、メニューの「並べて表示」をクリックすると、分割モードに切り替わります。

「分割モード画面」

分割前に選択されていたページを右側のTabControlに移動させて、並べて表示しています。
この動作を実現するために専用のユーザコントロール「TabHost」を作りました。

メインウィンドウのツリー構造は以下のようになっています。
(一部属性は省略)

  <Window>
    <DockPanel Name="_rootPanel">
        <Menu DockPanel.Dock="Top">
            (中身省略)
        </Menu>
        <ToolBar DockPanel.Dock="Top">
            (中身省略)
        </ToolBar>
        <local:TabHost x:Name="_tabHost"/>
    </DockPanel>
  <Window>

「TabHost」の中では、Grid内にTabControlを動的に配置しています。通常モードでは、Gridを1列1行にし、分割モードでは2列1行に
しています。

以下は通常モードの場合にTabControlを動的に追加する部分のコードです。画面に初めて表示された時点では通常モードで開くので
メインウィンドウのLoadedイベントのハンドラから下記メソッドをコールしています。

 public void OpenSingle()
 {
  解説:分割モードから通常モードに切り替える際にもコールされるメソッドなので、
  メソッドの先頭で行と列の定義をクリアする処理を行っています。

     //Gridを初期化する
     _tabPanel.Children.Clear();
     _tabPanel.ColumnDefinitions.Clear();
     _tabPanel.RowDefinitions.Clear();

  解説:一旦クリアした後、新たに1つずつ、行と列の定義を作って、Gridに設定します。

     //列を定義する
     ColumnDefinition col = new ColumnDefinition();
     _tabPanel.ColumnDefinitions.Add(col);

     //行を定義する
     RowDefinition row = new RowDefinition();
     _tabPanel.RowDefinitions.Add(row);

  解説:「_rightTab」は分割表示している際の右側のTabControlです。
  現在分割表示している場合を考慮し、右側のTabControlにあるページを一旦Listに退避し、
  左側のTabControlに移します。

     //右側のTabControlに追加されているTabItemを左側の
     //TabControlに移す
     List rightPages = new List();
     foreach (ExtendedTabItem page in _rightTab.Items)
     {
         rightPages.Add(page);
     }

     _rightTab.Items.Clear();

     foreach (ExtendedTabItem page in rightPages)
     {
         _leftTab.Items.Add(page);
     }

  解説:右側のTabControlを列と行を指定して、Gridに追加しています。

     //左側のTabControlをPabelに追加する
     _tabPanel.Children.Add(_leftTab);
     Grid.SetColumn(_leftTab, 0);
     Grid.SetRow(_leftTab, 0);

  解説:_isDividedは現在分割表示をしているかどうかを示すフラグ。ここではfalseに設定

     _isDivided = false;

  解説:IsDividableはTabHostが分割表示に出来る状態になっているかを示すフラグです。
  メインウィンドウの「並べて表示」と「並べて表示を解除」メニューを切り替えるために
  このフラグを使用します。現在分割表示されておらず、左側のTabControlにTabのページが
  2つ以上存在する場合、分割できる状態としています。

     //左側のTabに2つ以上のページが有る場合
     //分割可能フラグをたてる
     if (_leftTab.Items.Count >= 2)
     {
         IsDividable = true;
     }
     else
     {
         IsDividable = false;
     }
 }


次回も引き続きTabHostを解説します。

本日のソース






いつも、ブログを訪問していただきましてありがとうございます。

ちょっと遅ればせながらのご報告になるのですが、
先日、「YoutubeViewer」が1月28日に発売された晋遊舎さまのの「iP!」という雑誌で紹介されました。

あと、ホームページ解析ソフト「Krete」がVectorに登録されました。

今後とも、当ブログをよろしくお願いいたします。


「TabControlのTabを閉じられるようにする」は前回で完了と思っていたのですが、開発を進める
うちに問題が有ることに気づきました。

 private void closeButton_Click(object sender, RoutedEventArgs e)
 {
     //ClosePageEventを発生させる
     RaiseEvent(new RoutedEventArgs(ClosePageEvent, this));

     //TabControlを取得し、自身を削除する
     TabControl tabControl = Parent as TabControl;
     tabControl.Items.Remove(this);

     e.Handled = true;
 }

これは前回、紹介したルーティングイベントを発生させて、TabControlからページを削除する部分です。
ここでは、自身を削除する前に、ルーティングイベントを発生させているので、イベントを処理する側にイベントが
届いた時点では、まだTabControlにはページが有る状態なのです。

イベントを処理する側では、既に削除されていることを前提に処理したいので、これは問題でした。。
かといって、単純にルーティングイベントを発生させる前に、自身を削除してしまうとイベントが上位のツリー要素に
届かなくなってしまいます。

そこで、今度はTabControlを拡張して、ページの閉じるボタンが押された際のイベントを処理し、TabControlが該当ページを
自身から削除する方式に変えました。

まずは、TabPageの方を以下のように変更。クラス名も「ExtendedTabItem」としました。

 public static readonly RoutedEvent CloseButtonClickEvent = 
             EventManager.RegisterRoutedEvent("CloseButtonClick", RoutingStrategy.Bubble,
                             typeof(RoutedEventHandler), typeof(ExtendedTabItem));

 public event RoutedEventHandler CloseButtonClick
 {
     add { AddHandler(CloseButtonClickEvent, value); }
     remove { RemoveHandler(CloseButtonClickEvent, value); }
 }

 public override void OnApplyTemplate()
 {
     base.OnApplyTemplate();

     Button closeButton = base.GetTemplateChild("CloseButton") as Button;
     closeButton.Click += new RoutedEventHandler(closeButton_Click);
 }

 private void closeButton_Click(object sender, RoutedEventArgs e)
 {
     //CloseButtonClickEventを発生させる
     RaiseEvent(new RoutedEventArgs(CloseButtonClickEvent, this));

     //処理済にする
     e.Handled = true;
 }

「閉じる」ボタンがクリックされた時に、自身を削除する処理は行わず、閉じるボタンが押されたことを伝えるためのルーティングイベントを発生させるだけにしました。

そして、「TabControl」を拡張し、「ExtendedTabControl」というクラスを作成しました。
このクラスは、外観は「TabControl」のものをそのまま引継ぎ、挙動のみを拡張しました。

 static ExtendedTabControl()
 {
     //外観はカスタマイズしないので、TabControlのスタイルを適用する
     DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedTabControl), 
      new FrameworkPropertyMetadata(typeof(TabControl)));
 }
カスタムコントロールを作成すると自動的に挿入されている上記の部分なのですが、今回はここを少しいじって、TabControlの外観を利用するようにしました。
これをやらないと、XAMLの方でスタイルを定義しないといけなくなります。上記は簡単に言うと、ExtendedTabControlのスタイルにTabControlのスタイルを
適用してという意味です。

 public ExtendedTabControl()
 {
     AddHandler(ExtendedTabItem.CloseButtonClickEvent, new RoutedEventHandler(PageCloseButtonClick));
 }

先ほどExtendedTabItemで閉じるボタンが押された場合にルーティングイベントを発生させるようにしましたが、
ExtendedTabControlのコンストラクタで、そのイベントのハンドラを登録しています。

 public static readonly RoutedEvent PageCloseEvent = 
                         EventManager.RegisterRoutedEvent("PageClose", RoutingStrategy.Bubble,
                                       typeof(PageCloseEventHandler), typeof(TabControl));

 private void PageCloseButtonClick(Object sender, RoutedEventArgs e)
 {
     ExtendedTabItem page = e.OriginalSource as ExtendedTabItem;

     //自身からページを削除する
     Items.Remove(page);

     //ルーティングイベントを発生させる
     RaiseEvent(new PageCloseEventArgs(PageCloseEvent, this, page));
 }

ハンドラでは閉じるボタンがクリックされたページを自身から削除して、新たにページが閉じられた事を伝えるPageCloseEventを発生させています。
また、イベントの発生ソースに加えて、閉じられたページの情報をイベントの処理メソッドに伝えたかったのでRoutedEventArgsを拡張した
PageCloseEventArgsを作成しました。そしてそれを処理するためのデリゲートを新たに定義しました。

 public delegate void PageCloseEventHandler(Object sender,PageCloseEventArgs e);

 public class PageCloseEventArgs : RoutedEventArgs
 {
     private ExtendedTabItem _closedItem = null;

     public PageCloseEventArgs(RoutedEvent routedEvent, Object originalSource,
                                           ExtendedTabItem closedItem)
         : base(routedEvent,originalSource)
     {
         _closedItem = closedItem;
     }

     public ExtendedTabItem ClosedItem
     {
         get { return _closedItem; }
         set { _closedItem = value; }
     }

 }

これで本当にTabControlを閉じる部分は完了したと思います。

本日のソース





さて、今日は前回外観を作った「TabPage」に「閉じる」ボタンが押された場合のイベント処理を実装するところを
紹介します。

「閉じる」ボタンが押された場合、ページが閉じられたことを他のコントロールに伝えるためのルーティングイベントを
発生させます。そして、その後自身を「TabControl」から削除する処理を行います。

今回、開発している「仕訳入力アプリケーション」では同じページが複数開かないように、現在開いているページの
インスタンスをメインフォームのインスタンス変数に設定して管理しています。

「TabPage」が閉じられた際に「TabPage」から発生させるルーティングイベントを処理して、メインフォームのインスタンス変数に
「null」を設定するコードも最後に紹介します。

まずは、「閉じる」ボタンがクリックされた際のイベント処理から。

前回、「Generic.xaml」で以下のようにして閉じるボタンを追加しました。

<Grid>
  <Border>
    <DockPanel x:Name="ContentPanel">
      <Button x:Name="CloseButton" DockPanel.Dock="Right" Style="{StaticResource CloseDisableButton}"/>
      <ContentPresenter ContentSource="Header"/>
    </DockPanel>
  </Border>
</Grid>

「閉じる」ボタンのx:Name属性に[CloseButton]を設定してコードからアクセスできるようにしてあります。

「TabPage.cs」の中では、まず、「FramewoekElement」クラスの「OnApplyTemplate()」というメソッドをオーバーライドして
xamlのテンプレート内で定義した「閉じる」ボタンの「Click」イベントにハンドラを登録します。

 public override void OnApplyTemplate()
 {
     base.OnApplyTemplate();

     Button closeButton = base.GetTemplateChild("CloseButton") as Button;
     closeButton.Click += new RoutedEventHandler(closeButton_Click);
 }

テンプレート内で定義したUIElement(Buttonなど)は、テンプレートが適用された後でないと取得することが出来ません。
「OnApplyTemplate()」はコントロールにテンプレートが適用された後にコールされるメソッドなので
この中で「閉じる」ボタンのインスタンスを取得しています。

そして「閉じる」ボタンのクリックイベントを処理して、新たなルーティングイベントを発生させます。
ルーティングイベントを発生させるためには、あらかじめ、WPFのイベントシステムにイベントを登録しておく必要があります。
以下がその部分です。

 public static readonly RoutedEvent ClosePageEvent = EventManager.RegisterRoutedEvent("ClosePage", RoutingStrategy.Bubble,
                                                                             typeof(RoutedEventHandler), typeof(TabPage));

第1引数はイベントの名前。これはどこで利用されるのか正直わかりませんが、内容が分かりやすいものを付けておけば良いかと思います。
第2引数はルーティングの方法です。Tunnel、Bubble、Directのいずれかを指定します。Tunnelは要素ツリーの上から下へとイベントが伝えられる
ルーティング、Bubbleは下から上です。Directは従来の方法と同じく直接登録したハンドラにのみイベントが伝えられます。
第3引数はイベントのハンドラのデリゲートの型、第4引数にはイベントの所有者?(と書かれています。)これも何に使われるか分かりません。

ここで重要なのは、このメソッドの戻り値であるRoutedEventをクラス変数に設定しておくことです。後でイベントを発生させる際や
このイベントのハンドラを登録する際にキーの役割をします。

次がイベントを発生させる部分です。

 private void closeButton_Click(object sender, RoutedEventArgs e)
 {
     //ClosePageEventを発生させる
     RaiseEvent(new RoutedEventArgs(ClosePageEvent, this));

     //TabControlを取得し、自身を削除する
     TabControl tabControl = Parent as TabControl;
     tabControl.Items.Remove(this);

     e.Handled = true;
 }

RaiseEvent()はUIElementに定義されているメソッドで、引数に指定したルーティングイベントを発生させてくれます。引数にはRoutedEventArgs
を指定します。RoutedEventArgsにはEventManager.RegisterRoutedEvent()から返されたRoutedEventと、イベントのソースとなる自身の
インスタンスを設定します。

イベントを発生させた後、自身をTabControlから削除する処理を行っています。イベントを発生させる前にこれをやってしまうと、
要素ツリーの上下関係が失われ、ルーティングイベントが伝わらなくなってしまいます。また、「閉じる」ボタンのClickイベントも
ルーティングイベントなのですが、これ以上伝える必要が無いので処理済としてマークしています。

最後にこのイベントを処理しているMainWindow.xaml.csのソースを紹介します。
まずコンストラクタでClosePageEventイベントのハンドラを登録しています。この際、AddHandler()メソッドの第1引数に
TabPage.csでEventManager.RegisterRoutedEvent()から返されたRoutedEventを指定します。

 //ページが閉じた際のイベントにハンドラを登録する
 AddHandler(TabPage.ClosePageEvent,new RoutedEventHandler(TabPageClosed));

イベントを発生させる際にもこれを引数に利用していましたよね。このイベントのインスタンスを介して、イベントの発生ソースと
ハンドラを関連付けていることが分かるかと思います。

MainWindowの方では、このイベントを利用して、各ページのUserControlのインスタンス管理をしています。

 private void TabPageClosed(Object sender, RoutedEventArgs e)
 {
     TabPage page = e.OriginalSource as TabPage;

     if (page.Content is JournalEntry)
     {
         _journalEntry = null;
     }
     else if (page.Content is JournalRef)
     {
         _journalRef = null;
     }
 }

WPFを始めてまだ僅かですが、ここまででも、WPFのパワフルな機能にワクワクしてきます。
とっつき難さはあるのですが、今回のような拡張をWindowsフォームでやるとなると、倍以上苦労する気がします。

引き続きレポートします。

本日のソース





前回の日記で報告しましたように、TabControlにタブを動的に追加するところまで出来たのですが、
いざTabを追加してみると、一度開いたTabが閉じられないことがどうしても気になってしまいます。

各ページに閉じるボタンを付けてそれがクリックされたらTabが閉じるようにすれば良いのですが、
なんとなくかっこよくない気がします。そこでいきなりですが、TabItemを拡張してTabのヘッダの横に「閉じる」ボタンを
付けて、それがクリックされたらTabが閉じるようにしてみました。WPFの理解がまだまだなのでかなり苦労しました。。

まず、ソリューションエクスプローラから「追加」→「新しい項目」→「カスタムコントロール(WPF)」の
順に選んで、新規のカスタムコントロールを追加します。
コントロールの名前は「TabPage」にしました。すると、「TagPage.cs」というファイルと
「Themes/Generic.xaml」というファイルが追加されます。

「Generic.xaml」はコントロールの外観を定義するためのXAMLファイルです。
「TagPage.cs」はコントロールの挙動を定義するC#のソースコードです。作成された時点では「Control」クラス
を継承しているので「TabItem」を継承するように変更します。

今回は、まず「Generic.xaml」を編集してTabのヘッダに閉じるボタンを追加するまでを紹介します。



WPFではコントロールを拡張する際、ControlTemplateを作成し、外観を定義するのですが、
何分初心者なので最初は何をどうしたら良いのかさっぱり分かりませんでした。そこで、TabItemの
ControlTemplateのサンプルをここから持ってきて、それをそのまま
「Generic.xaml」に貼って動作を見てみることにしました。

外観がちょっと物足りないけどTabのページとして、TabControlに追加することが出来ました。
ただし、Contentに設定したコントロールがFillされた状態で表示されなかったので、
以下の2つのプロパティの設定を追加しました。

<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>

これをベースに閉じるボタンを追加していきます。

閉じるボタンの画像として、Tabが選択されている場合と選択されていない場合に表示する
2つのものを用意しました。

テンプレートの例を見てみると、TabItemのツリー構成は以下のようになっています。(属性は省きます)

<Grid>
  <Border>
    <ContentPresenter ContentSource="Header"/>
  </Border>
</Grid>

Gridの中にタブのヘッダを囲む枠に当たるBorderが配置されています。そしてその中に
TabItemのHeaderに設定されたUIElementの表示を担当するContentPresenterが置かれています。

これを見ると、なぜTabItemの「Content」のContentPresenterが無いのって思うかもしれません。
私も思いました。理由は分かりません。とにかくここに無くてもちゃんと表示されているので
今は気にしないことにします。
さて今回、閉じるボタンを追加するために、Borderの下にDockPanelを挟んで、DockPanelの右側に
閉じるボタンを配置し、その残りの部分にContentPresenterを配置します。

(一部属性は省略しています)
<Grid>
  <Border>
    <DockPanel x:Name="ContentPanel">
      <Button x:Name="CloseButton" DockPanel.Dock="Right" Style="{StaticResource CloseDisableButton}"/>
      <ContentPresenter ContentSource="Header"/>
    </DockPanel>
  </Border>
</Grid>

閉じるボタンのスタイルには、別途リソースの中に定義したものを適用しています。
スタイルはタブが選択されている場合のものと、タブが選択されていない場合のものを
用意し、それをトリガーを使って切り替えます。

以下がボタンのスタイルを定義するしている部分です。
<Style TargetType="Button" x:Key="CloseButton">
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border>
                    <Grid>                                    
                        <Image Source="/Image/close.gif" Width="12" Height="12"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="Button" x:Key="CloseDisableButton">
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border>
                    <Grid>
                        <Image Source="/Image/close_disable.gif" Width="12" Height="12"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

ButtonのContentにはImageを配置しています。イメージファイルはプロジェクトフォルダの直下に
Imageというフォルダを作り、そこに追加します。WPFの場合、これだけでアセンブリに含まれるリソースとして
ビルドしてくれます。
画像の切り替えは、トリガを使って行います。上記で紹介しましたようにデフォルトの状態ではStyleに
「CloseDisableButton」を設定し、選択された場合、以下のようにトリガを使って、「CloseButton」に
切り替えています。
<Trigger Property="IsSelected" Value="True">
  <Setter Property="Panel.ZIndex" Value="100" />
  <Setter TargetName="Border" Property="Background" Value="{StaticResource WindowBackgroundBrush}" />
  <Setter TargetName="Border" Property="BorderThickness" Value="1,1,1,0" />
  <Setter TargetName="CloseButton" Property="Style" Value="{StaticResource CloseButton}"/>
</Trigger>

閉じるボタンが押された際に実際にTabを閉じ、イベントを発生させる必要があるのですが、
その部分は次回に紹介させていただきます。


本日のソース




全体のレイアウト方針が決まったところで、作業開始。
初期状態ではWindowには「Grid」が貼り付けられているのでこれを「DockPanel」に貼り変えます。

些細なことですが、デザイナの上では貼り付けた「DockPanel」がWindowに対して、Fillされた状態に
ならないので、どうしたらいいのかな?といろいろ調べてみました。

結局、DockPanelの「Width」と「Height」が指定されている(Autoになっていないと)とFillされた状態にならないことに気づきました。
そりゃ、そうですよね。Windowsフォームに慣れているといろいろ戸惑いますね。。。

メインのウィンドウの構成は、「メニュー」と「ツールバー」そして、各画面を表示するエリアのコンテナになる「DockPanel」
を縦に並べました。前回、お話しましたように、各画面はユーザの操作により並べて表示したりしたいので
「TabControl」自体の貼り付けはC#の方から行いたいと思います。

・今日の時点での画面イメージ


予定としては各画面を同時に並べて表示する場合、2つのTabControlをDockPabelの左右に貼り付け、
各画面をそれぞれのTabControlに貼り付けます。通常の場合は、1つのTabControlに各画面をページとして並べます。

XAMLはこんな感じになります。

<Window x:Class="Paella.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Paella" Height="445" Width="499" Focusable="False">
    <DockPanel Name="_rootPanel">
        <Menu Height="22" Name="_menu" DockPanel.Dock="Top"/>
        <ToolBar Height="26" Name="_toolBar" DockPanel.Dock="Top">
            <Button Name="_inputBtn" Click="_inputBtn_Click" FontSize="12">振替伝票入力</Button>
            <Button Name="_refBtn">振替伝票の参照</Button>
        </ToolBar>
        <DockPanel Name="_tabPanel"/>
    </DockPanel>
</Window>

分かりやすいですよね。下手にデザイナで編集するより、XAMLを直接いじる方が効率が良い気がします。
このように直感的にレイアウトがどうなっているかを把握できるのはWPFの良さの1つですね。

次にDockPanelにTabControlを貼り付けるコードです。

_tabPanel.LastChildFill = true;
_leftTab = new TabControl();
_tabPanel.Children.Add(_leftTab);

DockPanelの「LastChildFill」をtrueに設定すると、最後に追加された
コントロールがFillの状態になるので、1つのコントロールしか配置しない場合は
これでOKです。

さて、いよいよ本日の本題の「TabControl」に動的にControlを追加する方法をです。
ツールバーにボタンを追加して、そのボタンがクリックされたら、振替伝票入力画面が
開くようにします。

private void _inputBtn_Click(object sender, RoutedEventArgs e)
{
    try
    {
        //振替伝票入力のユーザコントロール
        JournalEntry entry = new JournalEntry();
        AddTagPage(entry, "振替伝票入力");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
    }
}

private void AddTagPage(UserControl control,String header)
{
     TabItem item = new TabItem();
     item.Header = header;
     item.Content = control;

     _leftTab.Items.Add(item);

     //追加されたページが最初のページの場合、
     //何故か、そのコンテンツが表示されない現象が
     //発生する。それを回避するため、2つ目のページを追加し
     //即座に削除している
     if (_leftTab.Items.Count == 1)
     {
         item = new TabItem();
         item.Header = "ダミー";
         item.Content = "ダミー";
         _leftTab.Items.Add(item);

         _leftTab.Items.Remove(item);
     }
}

「JournalEntry」は振替伝票画面を実装するユーザコントロールです。
まず、TabItemのインスタンスを生成します。そして、その「Header」プロパティに
Tabページのタイトルを、Contentに、「JournalEntry」のインスタンスを設定します。
先ほどと同じくユーザコントロールのWidthとHeightに具体的な数値が指定されていると
Fillされた状態にならないので、気をつけてください。

そして、最後に、TabControlのItemsにTabItemを追加して完了です。

通常はこれでOKなのですが、タブページが全く追加されていない状態で
イベントハンドラからこの処理を行った場合、ヘッダは表示されるのですが、
コンテントが表示されない現象が発生します。しかし、2つ目のTabページを追加すると、
1ページのコンテントも表示されます。コンストラクタから同じ処理を行った場合は、
1ページ目から問題なく表示されています。???原因が分かりません。う~ん、TabControlのバグかな?

とりあえず、対処として、1ページ目を追加した直後にダミーの2ページ目を追加して
すぐに削除するという処理を行うようにして回避しました。

本日のソース




さて早速、作ってみようとWPFアプリケーションの新規プロジェクトを作成しました。
「Window1.xaml」というファイルがあって、これがWindowsフォームでいうところの「Form1」だな思って、
いつもやっているみたいに「MainWindow.xaml」と名前を変えてしまいました。

Windowsフォームならこれで何もせずに実行できるので、試しに実行してみると、
IOException[リソース 'window1.xaml' を検索できません。]というメッセージが表示されてしまいました。

「なんだなんだ」と思い、あちこち見てみると「App.xaml」というファイルの「StartupUri」という属性に
「Windows1.xaml」とファイル名が記述されていました。あ~これだなと思って変えてみたところ、
実行できました。

おまけに、「MainWindow.xaml.cs」を開いてみると、クラス名はWindows1のままでした。結局は全て
自分で直すということでした。こういうところの改善は今後に期待ということで。(あっ、Expressを使って
いるからかもしれません。)

ちなみに今回作る「仕訳入力」のソフトですが、「Paella(パエリア)」という名前にしました。
スペイン料理のパエリアからとりました。WPFはカラフルなイメージがあるので、この名前にしてみました。

まずは画面レイアウトの設計から始めたいと思います。個別の画面を設計する前に全体構成を考える必要があります。

今回開発するアプリには「振替伝票入力」画面と「振替伝票の参照」画面、「残高試算表」の3つの画面があります。
「残高試算表」はいいとして、「振替伝票入力」と「振替伝票の参照」は同時に開きたい場合があるかなと
思っています。

そうなると、真っ先に「MDIを使おう」と思ってしまうのですが、調べたところWPFはMDIをサポートしていませんでした。。
WPFの開発に携わった方のブログにWPFでMDIが実装されていない理由が書かれています。

それによると、まず、将来的にもWPFにMDIを入れないということではなく、今回は時間が無かったのでみたいなことが
書かれていました。また、MDIと言ってもいろんなものがあるので、みなさんからのフィードバックも欲しいとおっしゃって
います。

で、現状どうしたらよいかということについては、Windowsフォームとの相互運用(ここにサンプルがあります)を使用するか、
または「自分でつくりなさい」(これもここにサンプルがあります。)と言っています。

う~ん、WPFを検証するためのアプリを作ると言っていきなり相互運用に走るのは主旨と違う気がしますし、
そうかと言っていきなりMDIを自作するほど、理解が進んでいるわけではないので、いきなり悩んでしまいました。

とりあえず、「TabControl」を使うことにして、いったんそれぞれの画面をタブのページとして表示することにします。
そして、同時に参照したい場合は、Excelの左右に並べて表示のイメージで行きたいと思います。
ユーザがメニューから左右表示をクリックするとWindow全体を左右に2つに割ってそれぞれにTabControlを貼り付けます。
そして、その中に同時に見たいページをそれぞれ貼り付けなおします。

これぐらいだったら、不慣れな私でも出来そうなので、たぶん。
とりあえず、挑戦してみます。

ではでは、またレポートします。

WPF(Windows Presentation Foundation)には興味を持っているのですが、なかなかどういったソフトに適しているのか
分からずまだ挑戦出来ていません。

一般的にWPFを使うとより高度な表現力を持つアプリケーションを開発できると言われていますが、ほとんどの業務システムにおいては、
従来のWindowsフォームでも十分にユーザの要求を満たしているのでは?と疑問に思ってしまいます。

そこで今回、WPFの魅力を探るために実験的にソフトを開発してみて、自分なりにその使い道や良さを検証してみたいと思います。
また、開発したソフトをみなさまに触って頂いて参考にしていただければうれしいと思っています。

題材として業務システムの中でもより高度な操作性を求められる会計系のソフトを選びました。

もちろん、本格的な会計系のソフトをそうやすやすと開発できるわけではないので、以下の3つ機能のみを持つ
仕訳入力用のソフトにします。

・振替伝票入力
・振替伝票の参照
・残高試算表の参照

「振替伝票入力」機能は、勘定科目のマスタに登録されている勘定科目で仕訳を入力する機能です。
入力項目は「借方勘定科目」、「借方金額」、「貸方勘定科目」、「貸方金額」、「摘要」にします。

「振替伝票の参照」機能は過去に入力した振替伝票を参照する機能です。
「残高試算表の参照」機能は名前の通りの機能です。

このソフトはあくまでもWPFの検証目的のためで、実際に使っていただく(使えないと思います)ための
ものではありません。


データベースには「System.Data.SQLite」という組み込み用のデータベースを使用します。
触っていただく際に、環境構築の必要性が無いため、これにしました。

機能は少ないのですが、WPFが初めてということもあり、長丁場になりそうです。
開発過程も参考になればと思いますので、まめに日記をアップしていきます。
それでは次回からよろしくお願いします。


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