クライアント領域に関するメモ的な何か

パッケージ開発担当の読です。

めんどくさがり屋でずっと携帯電話を変えていなかったのですが、
充電ができなくなるというどうにもならない状況になったので、
この期にスマートフォンに変更してきました。

使い方がよくわかっていないですが、まぁ何とかなるでしょう!世の中にはGoogle先生という
ある程度なら何でも教えてくれる方がいらっしゃいますので。

今回はWPFを使用しないでタイトルバーにボタンを配置するです。
正直このままでは使える代物ではないので、参考にはしない方がいいです。
調べはしましたが、このままなかったことになるのも悔しいので・・・そのうちまたどうにかできないかしっかり調べたいです。

調べるにあたり参考にさせていただいたページ

傾き指向プログラミング様
satackoverflow様
MSDN Managine様

フレームの調整

ウィンドの四辺にあるフレームの大きさを調整します。
このサンプルでは上辺のフレームを60ピクセルに拡大しています。


// DWM合成が利用可能がチェックする
[DllImport("dwmapi.dll")]
private static extern int DwmIsCompositionEnabled(out bool enabled);
// 半透明ウィンドにする(ウィンドハンドル,範囲)
[DllImport("dwmapi.dll", PreserveSig = false)]
private static extern void DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);

// 半透明範囲設定
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
   public int leftWidth;
   public int rightWidth;
   public int topHeight;
   public int bottomHeight;
}

// コンストラクタ
public TitleBarTest()
{
   IntPtr handle = this.Handle;
   // わかりやすいよう赤で設定しています。
   this.BackColor = Color.Red;
   // エアロ効果が使用できるのかチェックします
   bool DwmEnabled = false;
   DwmIsCompositionEnabled(out DwmEnabled);
   if (DwmEnabled)
   {
      MARGINS margin;
      margin.topHeight = 60;
      margin.bottomHeight = 0;
      margin.leftWidth = 0;
      margin.rightWidth = 0;
      // 上記の範囲でフレームを設定
      DwmExtendFrameIntoClientArea(handle, ref margin);
   }
}

※コンストラクタでなくてもフレームを読み込む前ならどこでも構いません。
上記のサンプルを動かすと以下のようなウィンドになります。

画像の赤色で表示されているところが、私達がボタンなどを配置できるクライアント領域になります。
今回はクライアント領域がわかりやすいように背景を赤にしていますが、背景を黒にすることでDWMが勝手に透明処理を行ってくれます。

注意)フレームに重なっている箇所はDWMが勝手に半透明処理を行ってしまうので、ラベルの文字なども透明にされてしまいます。
ですのでGDI+などを使用してアルファ チャネルで描画する必要があります。

クライアント領域の調整

今の状態では最小化や最大化ボタンのある位置にボタンは配置できません、
ですので以下のソースを上記のソースに追記し、クライアント領域をずらします。


struct NCCALCSIZE_PARAMS
{
   public RECT rcNewWindow;
   public RECT rcOldWindow;
   public RECT rcClient;
   IntPtr lppos;
}

public struct RECT
{
   public int left;
   public int top;
   public int right;
   public int bottom;
}

private void AdjustClientRect(ref RECT rcClient)
{
   rcClient.top -= 30;
}

protected override void WndProc(ref Message m)
{
   base.WndProc(ref m);

   // クライアント領域を調整する(WM_NCCALCSIZE)
   if (m.Msg == 0x0083)
   {
      if (m.WParam != IntPtr.Zero)
      {
         NCCALCSIZE_PARAMS rcsize = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
         AdjustClientRect(ref rcsize.rcNewWindow);
         Marshal.StructureToPtr(rcsize, m.LParam, false);
      }
      else
      {
         RECT rcsize = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
         AdjustClientRect(ref rcsize);
         Marshal.StructureToPtr(rcsize, m.LParam, false);
      }
      m.Result = new IntPtr(1);
      return;
   }
}

上記のサンプルを動かすと、以下のようなウィンドになっていると思います。

先ほど書いたように、赤色の部分はクライアント領域になっているので、これでタイトルバーにボタンを配置することができます。
最終的に背景は黒にして、ボタンを配置するとこんな感じになります。

問題点

クライアント領域をタイトルバーへ埋め込んだことで、タイトルバーの機能が使えなくなっています、
タイトルバーをドラッグで移動や上部ドラッグでウィンドサイズ変更、最小化、最大化、閉じるなどを
自分で実装する必要があります。

たとえば以下はHeadというpanelをタイトルバー部分にあたる箇所に配置して疑似タイトルバーとして扱ってドラッグ機能を実装したものです。


[DllImportAttribute("user32.dll")]
public static extern bool ReleaseCapture();
[DllImportAttribute("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);

// 疑似タイトルバーのドラッグ
private void Head_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
   if (e.Button == MouseButtons.Left)
   {
      //ウィンドウがマウス入力を受け取る
      ReleaseCapture();
      SendMessage(this.Handle, 0xA1, 0x2, 0);
   }
}

最後に

タイトルバーの機能がちゃんと使えれば問題ないのですが、このままでは使えないので
しっかり時間を取って調べたいですね。
「こんなこともできるんだな」程度見てくだされば幸いです。