Adsense_top

2009年5月30日土曜日

C# DataGridView セルの結合 その3

C# DataGridView セルの結合 その2」の続きです。
結合されたセルの選択時に結合前の各セルの値が編集コントロールにセットされる問題が残していました。Excelなどでセルを結合させると左上端のセルの値を保持し、それ以外のセルの値は空にしているのに倣おうと思います。つまり、結合されたセル範囲で上端セルに値を保持します。編集は、そのセルに対して編集処理を行います。
C# DataGridView セルの結合 その2」の「DgvMergebleTextBoxCell」クラスに追加します。
public override void InitializeEditingControl(
  int rowIndex,
  object initialFormattedValue,
  DataGridViewCellStyle dataGridViewCellStyle) {

  base.InitializeEditingControl(rowIndex,
  initialFormattedValue,
  dataGridViewCellStyle);
  DataGridViewTextBoxEditingControl ctl =
  this.DataGridView.EditingControl
  as DataGridViewTextBoxEditingControl;
  // 結合された列の上端の行インデックスを算出しセルを取得
  int row = (rowIndex / mergeRows) * mergeRows;
  DataGridViewCell cell =
  this.DataGridView[ColumnIndex, row];
  // 初期化した編集コントロールに、
  // 結合された列の上端セルの値を代入
  ctl.Text = Convert.ToString(cell.Value);
}

上記コードは、編集コントロールの初期化時に、結合セル範囲の上端セルの値を編集コンロロールの編集初期値として設定するようにオーバーライドしています。
protected override object GetValue(int rowIndex) {
  if(rowIndex % mergeRows == 0) {
    // 結合された列の上端行のセルの場合のみ値を返す
    return base.GetValue(rowIndex);
  } else {
    // 結合された列の上端行のセル以外の場合は、null値を返す
    return null;
  }
}
protected override bool SetValue(int rowIndex, object value) {
  if(this.DataGridView == null || rowIndex % mergeRows == 0) {
    // DataGridViewにまだ追加されていないセルの場合と
    // 結合された列の上端行セルの場合は通常通り値を設定します。
    return base.SetValue(rowIndex, value);
  } else {
    try {
      // 結合された列の上端行のインデックスを計算
      int topRowIndex = (rowIndex / mergeRows) * mergeRows;
      // DataGridViewにすでに追加されていて、結合された列の
      // 上端行以外のセルの場合、結合された列の上端行のセルに
      // 値を設定します。
      this.DataGridView[this.ColumnIndex,
      TopRowIndex(rowIndex)].Value = value;
      return true;
    } catch(Exception ex) {
      // 何らかのエラーが発生した場合は、falseを返します。
      return false;
    }
  }
}

続いて、結合セル範囲の上端セルに値が集中するようにしました。また、その他のセルでは、空の値を返すように、基本クラスのメソッドをオーバーライドしました。

今日はここまでにします。見た目的に、あまり変化がなく面白くないので、次回は表示に関する描画部を考えて見ようと思っています。

2009年5月29日金曜日

C# DataGridView セルの結合 その2

「その1」で書いた分にフォーカスが入った場合の描画処理を追加が考えていたより厄介なようです。
手間が掛かるのであればと言う事で、DataGridViewTextBoxColumnを拡張して結合可能セルの作成に挑戦する事にしました。
と言っても、本当の意味での結合をさせるには、セルを拡張するのではなくDataGridView自体を拡張を行う必要がある事は明らかですが、ハッキリ言って私には荷が重過ぎます(キッパリ)。
取りあえず、編集可能で見た目はセルが結合されているように見えるものにはしたいと思います。
先ずは、行の結合が出来るものから少しずつ...(列の結合は行の結合が完成したら...)。
アドバイス大歓迎ですので、あれば下さい。

DataGridViewTextBoxColumnとDataGridViewTextBoxCellを継承して拡張します。
DataGridViewTextBoxEditingControl に関しては、今のところは拡張せずに使う予定です。

一気には出来ませんので、今回は編集時に結合されているように編集コントロール(TextBox)のサイズを合わせて表示するようにします。
public class DgvMergebleTextBoxColumn : DataGridViewTextBoxColumn{
  // 結合行数
  private int mergeRows = 1;
  // コンストラクタ
  public DgvMergebleTextBoxColumn() {
    this.CellTemplate = new DgvMergebleTextBoxCell();
  }
  // 結合行数を設定または取得します。
  public int MergeRows {
    get {
      return
      ((DgvMergebleTextBoxCell)this.CellTemplate).MergeRows;
    }
    set {
      if(this.mergeRows == value)
        return;
      //セルテンプレートの値を変更する
      ((DgvMergebleTextBoxCell)this.CellTemplate).MergeRows
      = value;
      //DataGridViewにすでに追加されているセルの値を変更する
      if(this.DataGridView == null)
        return;
      int rowCount = this.DataGridView.RowCount;
      for(int i = 0; i < rowCount; i++) {
        DataGridViewRow r = this.DataGridView.Rows.SharedRow(i);
        ((DgvMergebleTextBoxCell)r.Cells[this.Index]).MergeRows
        = value;
      }
    }
  }
  // MergeRowsプロパティを追加しているので、
  // Clone()をオーバーライドします。
  public override object Clone() {
    DgvMergebleTextBoxColumn col =
    (DgvMergebleTextBoxColumn)base.Clone();
    col.MergeRows = this.MergeRows;
    return col;
  }
}

public class DgvMergebleTextBoxCell : DataGridViewTextBoxCell {
  // 結合行数
  private int mergeRows = 1;
  public override object Clone() {
    DgvMergebleTextBoxCell cel =
    (DgvMergebleTextBoxCell)base.Clone();
    cel.MergeRows = this.MergeRows;
    return cel;
  }
  /// 

  /// DataGridView 内でセルがいくつの行にまたがって表示されるかを
  /// 示す値を取得または設定します。
  /// 

  public int MergeRows {
    get { return mergeRows; }
    set { mergeRows = value; }
  }
  // ホストされる編集コントロールの位置とサイズを設定します。
  public override void PositionEditingControl(
  bool setLocation, bool setSize,
  Rectangle cellBounds,
  Rectangle cellClip,
  DataGridViewCellStyle cellStyle,
  bool singleVerticalBorderAdded,
  bool singleHorizontalBorderAdded,
  bool isFirstDisplayedColumn,
  bool isFirstDisplayedRow) {
    // 結合するセルの高さに合わせるために、結合するセルの高さを足す
    Rectangle mergeCellBounds = cellBounds;
    for(int i = 1; i < mergeRows; i++) {
      if(RowIndex + i < this.DataGridView.Rows.Count) {
        mergeCellBounds.Height +=
        DataGridView.Rows.SharedRow(RowIndex + i).Height;
      }
    }
    if(RowIndex % mergeRows == 0) {
      base.PositionEditingControl(setLocation, setSize,
      mergeCellBounds,
      mergeCellBounds, cellStyle,
      singleVerticalBorderAdded,
      singleHorizontalBorderAdded,
      isFirstDisplayedColumn,
      isFirstDisplayedRow);
    } else {
      int row = (RowIndex / mergeRows) * mergeRows;
      for(int k = row; k < RowIndex; k++) {
        mergeCellBounds.Y -=
        DataGridView.Rows.SharedRow(k).Height;
      }
      base.PositionEditingControl(setLocation, setSize,
      mergeCellBounds,
      mergeCellBounds, cellStyle,
      singleVerticalBorderAdded,
      singleHorizontalBorderAdded,
      isFirstDisplayedColumn,
      isFirstDisplayedRow);
    }
  }
}

以下のようにデータを入れてテストしています
int num = 1;
for (int i = 0; i < 9; i++) {
  dataGridView1.Rows.Add(num++, num++);
}

2列目に先に書いた「DgvMergebleTextBoxColumn」を設定し1行目を編集状態にした図です。

キャプチャするときに消えてしまっていますが、値「2」の後ろにキャレットが点滅しています(編集状態)


実際に動かすと分かると思いますが、選択したセルの値を編集することになり、問題があります。
次回はその点を改善したいと思います。

2009年5月21日木曜日

C# DataGridView セルの結合 その1

DataGridViewでセルを結合にチャレンジしてみました。
まずは見た目だけのセル結合です。ラベルのように表示するだけであれば、この方法で十分だと思います。
セルの描画部を処理するだけです。


上記図は、以下の4列のDataGridViewTextBoxColumnを追加し以下のコードでデータを挿入しています。
int num = 1;
for (int i = 0; i < 10; i++)
dataGridView1.Rows.Add(num++, num++, num++, num++);

これを基に、1列目(Column1)では2行を結合し1セルとします。次に1行おきに2列目、3列目を結合します。


結果このような感じになります。
以下のように「CellPainting」イベントを処理しています。
コードを見ていただければわかりますが大した事はしていません。シコシコと描画処理をしているだけです。

private void dataGridView1_CellPainting(object sender, 
 DataGridViewCellPaintingEventArgs e) {
  DataGridView dv = (DataGridView)sender;
  // 行・列共にヘッダは処理しない
  if(e.RowIndex < 0 || e.ColumnIndex < 0)
    return;

  Rectangle rect;
  DataGridViewCell cell;
  // 1列目の処理
  if(e.ColumnIndex == 0) {
    rect = e.CellBounds;
    // 奇数行(1,3,5..行目 = RowIndexは0,2,4..)
    if(e.RowIndex % 2 == 0) {
      cell = dataGridView1[e.ColumnIndex, e.RowIndex + 1];
      //一つ下の次のセルの高さを足す
      rect.Height += cell.Size.Height;
    }
      // 偶数行の処理
    else {
      cell = dataGridView1[e.ColumnIndex, e.RowIndex - 1];
      // 一つ上のセルの高さを足し、矩形の座標も一つ上のセルに合わす
      rect.Height += cell.Size.Height;
      rect.Y -= cell.Size.Height;
    }
    // セルボーダーライン分矩形の位置を補正
    rect.X -= 1;
    rect.Y -= 1;
    // 背景、セルボーダーライン、セルの値を描画
    e.Graphics.FillRectangle(
    new SolidBrush(e.CellStyle.BackColor), rect);
    e.Graphics.DrawRectangle(
    new Pen(dv.GridColor), rect);
    TextRenderer.DrawText(e.Graphics,
    cell.FormattedValue.ToString(),
    e.CellStyle.Font, rect, e.CellStyle.ForeColor,
    TextFormatFlags.HorizontalCenter
    | TextFormatFlags.VerticalCenter);
    // イベント ハンドラ内で処理を行ったことを通知
    e.Handled = true;
  }
    // 2列目と3列目の結合処理
  else if(e.ColumnIndex == 1) {
    // 奇数行のみ列結合
    if(e.RowIndex % 2 == 0) {
      rect = e.CellBounds;
      cell = dataGridView1[e.ColumnIndex + 1, e.RowIndex];
      // 一つ右のセルの幅を足す
      rect.Width += cell.Size.Width;
      rect.X -= 1;
      rect.Y -= 1;
      e.Graphics.FillRectangle(
      new SolidBrush(e.CellStyle.BackColor), rect);
      e.Graphics.DrawRectangle(new Pen(dv.GridColor), rect);
      TextRenderer.DrawText(e.Graphics,
      e.FormattedValue.ToString(),
      e.CellStyle.Font, rect, e.CellStyle.ForeColor,
      TextFormatFlags.HorizontalCenter
      | TextFormatFlags.VerticalCenter);
      e.Handled = true;
    } else {
      // 2列目の偶数行は、結合を行わないので、通常の描画処理に任せる
      e.Paint(e.ClipBounds, e.PaintParts);
    }
  } else {
    // 3列目の奇数行は描画処理をせずに、
    // イベントハンドラ内で処理を完了したこと通知
    if(e.RowIndex % 2 == 0 && e.ColumnIndex == 2)
      e.Handled = true;
  }
}

正しくは、値の描画の処で「e.CellStyle.Alignment」を確認すべきですが、ここでは簡単にするためにセンターに描画しています。

他に、セルをReadOnlyにする処理や、結合セルのフォーカスがある場合の描画処理、キー入力でのフォーカスの移動の処理が必要です。

上記、未実装の部分は別の日に考えます。あしからず...

2009年5月2日土曜日

C# 超簡易パケット(メール)中継・ウォッチプログラム

ちょっとしたプログラムを作ってみました。
今、メール機能を含むソフトを考えていますので、そのソフトのテストに使えるのではと思い作りました。

機能的には、PC上のクライアントプログラムとサーバ間に流れるパケットデータを覗き見することができます。覗き見と言うと隠れて密かに見るようですが、そこまでの機能はありません。
どちらかと言うとパケットデータを中継し、パケットの内容を表示する感じのイメージです。
クライアントプログラムの接続先として当プログラムを設定し、当プログラムから実際の接続先へ接続します。反対にサーバーからの返答も当プログラムが一旦受信してからクライアントプログラムに送信します。

とりあえず簡単なプログラムなので、実際に動かしてみてください。

まずは、ウインドウのデザインです。

・主な規定値から変更したプロパティ
 portNumericUpDown
            Minimum : 0
            Maximum : 65535
 stopButton
            Enabled : false
 messageTextBox
            Anchor : Top, Bottom, Left, Right

まず参照(using ディレクティブ)部です。
using System;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;

次は変数宣言部です。
// スレッド停止通知用フラグ
bool stop = true;
// プリフィックス[inner]がクライアント方向用
// プリフィックス[outer]がサーバー方向用

// クライアント接続
TcpClient innerClient = null;
TcpClient outerClient = null;
// リスナー設定
TcpListener innerListener = null;
// 読み書き用ストリーム
NetworkStream innerStream = null;
NetworkStream outerStream = null;
// スレッド
Thread innerThread = null;
Thread outerThread = null;
// スレッド間でTextBoxへの文字列出力のためのデリゲート
delegate void SetTextCallback(string text);
// 接続先ホスト名を保持する
string host;
// 接続先ポート番号を保持する
int port;
コメントとして「クライアント方向」「サーバー方向」と書いていますが、クライアント方向は、OutlookなどのPC内のソフトとの送受信を担当するという意味です。反対にサーバー方向はメールサーバーなどのインターネット側、つまりPCの外側を担当します。

コンストラクタでは、今回は特に何もしていないので飛ばしますが、必要に応じてホスト名やポート番号の初期値をセットする部分を入れてください。
コントロールイベント部です。
//
// 接続監視を始める
//
private void startButton_Click(object sender, EventArgs e)
{
stop = false;
host = hostTextBox.Text;
port = (int)portNumericUpDown.Value;
stopButton.Enabled =
!(startButton.Enabled = hostTextBox.Enabled =
portNumericUpDown.Enabled = false);
innerListener = new TcpListener(IPAddress.Any, port);
// クライント方向のスレッドを開始
innerThread = new Thread(new ThreadStart(Inner));
innerThread.Start();
}

//
// 接続監視および中継を中止する
//
private void stopButton_Click(object sender, EventArgs e)
{
stop = true;

stopButton.Enabled =
!(startButton.Enabled = hostTextBox.Enabled =
portNumericUpDown.Enabled = true);
Abort();
SetText("--待機中止--");

}
//
// 接続待ち状態であれば、スレッドを止めてかウインドウを閉じる
//
private void Form1_FormClosing(object sender,
FormClosingEventArgs e)
{
if (!stop) Abort();
}
「startButton_Click」で接続待ち状態に入り受信内容を読み取る「Inner」メソッドを別スレッドとして開始します。「stopButton_Click」では、処理を行っているスレッドを終了させます。「Form1_FormClosing」は、フォームを閉じる前に、別スレッドの処理が完了しているか確認を行っています。
次は、このプログラムの中心機能部分である、データの送受信部分です。
//
// クライアント方向に対する処理
//
private void Inner()
{
try
{
// 接続待機
innerListener.Start();
SetText("--接続待機中--");
innerClient = innerListener.AcceptTcpClient();
// 接続されたら、サーバー方向接続を開始
StartOuter();
//接続
SetText("--接続されました--");
innerStream = innerClient.GetStream();

Byte[] bytes = new Byte[1024];
int i;
// 停止フラグがtrueに変更されたら処理を抜ける
while (!stop)
{
// Console.WriteLine(innerClient.Connected);
try
{
if ((i = innerStream.Read(bytes, 0, bytes.Length)) != 0)
{
// ホストにデータを流す
outerStream.Write(bytes, 0, i);
// TextBoxに中継したデータを追加する
String data =
System.Text.Encoding.UTF8.GetString(bytes, 0, i);
SetText(String.Format("C: {0}", data));
}
}
catch(IOException)
{
break;
}
}
// 閉じる
innerStream.Close();
innerClient.Close();
stop = true;
}
catch (ThreadAbortException)
{
// 何もしない
;
}
catch (SocketException)
{
// 何もしない
;
}
// 再度接続待ちをはじめる
ReStart();
}

//
// ホスト側処理のスレッドを実行する
//
private void StartOuter()
{
outerThread = new Thread(new ThreadStart(Outer));
outerThread.Name = "OUTER";
outerThread.Start();
}

//
// ホスト方向に対する処理
//
private void Outer()
{
try
{
outerClient = new TcpClient();
outerClient.Connect(host, port);
outerStream = outerClient.GetStream();
SetText("--ホストに接続しました--");
Byte[] bytes = new Byte[1024];
int i;

//メッセージを受信
while (!stop)//!stop && outerStream.CanRead)
{
if ((i = outerStream.Read(bytes, 0, bytes.Length)) != 0)
{
// クラインとデータを流す
innerStream.Write(bytes, 0, i);
// TextBoxに中継したデータを追加する
String data =
System.Text.Encoding.UTF8.GetString(bytes, 0, i);
SetText(String.Format("S: {0}", data));
}
else
{
break;
}
}
// 閉じる
outerStream.Close();
outerClient.Close();
stop = true;
}
catch (ThreadAbortException)
{
// 何もしない
;
}
catch (SocketException)
{
// 何もしない
;
}
}
「Inner」では、クライントプログラムからの接続を待ち、接続要求を受け付けた後は、サーバー側の接続を開始し、クライアント側の受信ストリームからデータを読み込み、サーバー側にデータを流し同時にフォームのTextBoxに、データを文字列に変換し追加しています。「StartOuter」でサーバー側の接続を受け持つ「Outer」メソッドを別スレッド開始います。「Outer」ではサーバー側に接続し、サーバー側の返答を受信しクライアントにデータを流し同時にフォームのTextBoxに、データを文字列に変換し追加しています。
残りのメソッドです。
//
// 再度接続待ちに入る
//
private void ReStart()
{
stop = false;
SetText("再スタート");
innerThread = new Thread(new ThreadStart(Inner));
innerThread.Start();
}

//
// TextBoxに表示する
//
private void SetText(string text)
{
if (this.messageTextBox.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.messageTextBox.AppendText(text + "\r\n");
}
}

//
// 通信用スレッドを終了させる
//
private void Abort()
{
if (outerThread != null && outerThread.IsAlive)
{
outerThread.Abort();
outerThread.Join();
}
innerListener.Stop();
if (innerThread.IsAlive)
{
innerThread.Abort();
innerThread.Join();
}
// 念のため
if (outerStream != null) outerStream.Close();
if (outerClient != null) outerClient.Close();
if (innerStream != null) innerStream.Close();
if (innerClient != null) innerClient.Close();
}
「ReStart」は、送受信完了後の再接続待ちに入るためのメソッドです。「SetText」は、送受信を行っている別スレッドからデリゲートを介して、フォーム上のTextBoxに文字列を追加するものです。「Abort」は、強制的に送受信を行っている別スレッドを止めるためのメソッドです。
以下の図は、Outlookで送信メールサーバーを「127.0.0.1」に設定し、今回作成したしたプログラムのホスト欄に実際のサーバー名を入力し、Outlookの「アカウント設定のテスト」を行った時の実際の送信内容を中継しているところです。