1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ColumnHeaderにプロパティを追加したいっ!

Last updated at Posted at 2016-08-29

背景

前稿迄でカラムの直接編集ちっくは実現できました。でも、全てのカラムが編集対象になるとは限りません。
現状それをコントロールするには、どのカラムを編集対象とするかを外部に保持して、外部からコントロールする必要があります。

許せんっ!
美しくないっ!

きちんと役割毎に分離しなくては…

方針

前稿からの流れで行くと…

  • ColumnHeaderをサブクラス化する
  • サブクラスにEditableプロパティを設ける
  • (一応確認まで)Editableプロパティがtrueのカラムだけの編集を可能とする

と云う様な事がしたいんですよね。


んー、プロパティ追加するだけなら何てこたないんです。

public class ColumnHeaderEx : ColumnHeader {
	public ColumnHeaderEx() : base() {
	}

	[Browsable(true)]
	public bool Editable { get; set; }
}

ってなサブクラスを定義して、Form1.cs の中に以下の様なコードを追加しておけばオッケー。

あくまでもイメージです
private ColumnHeaderEx columnHeaderEx1;
private ColumnHeaderEx columnHeaderEx2;
private ColumnHeaderEx columnHeaderEx3;

this.columnHeaderEx1 = new ColumnHeaderEx();
this.columnHeaderEx2 = new ColumnHeaderEx();
this.columnHeaderEx3 = new ColumnHeaderEx();

this.columnHeaderEx1.Text = "カラム 1";
this.columnHeaderEx2.Text = "カラム 2";
this.columnHeaderEx3.Text = "カラム 3";

this.listView1.Columns.AddRange(new ColumnHeader[] { this.columnHeaderEx1, this.columnHeaderEx2, this.columnHeaderEx3});

ただ残念なのは、これではデザイナで列の定義が出来ないんですよ。
また折角定義したEditableプロパティを参照する際には、 (ColumnHeaderEx)(this.listView1.Columns[i]).Editableって記述しないといけないし…

と云う事で今回の目的は以下とします。

  • ColumnHeaderExサブクラスを実現します
  • デザイナ画面でカスタムプロパティ含めて列定義が出来るようにします
  • 勿論、デザイナ画面ってのはここの事 ↓

    imageここを選んで


    imageからの


    imageこいつ

この右側にカスタムプロパティが出て欲しい訳です。
…頑張りましょっ!

実験

まず、ネットを漁りました。
イイ感じの日本語の情報には余り行き当たりません…

断片的な話と、ColumnHeaderをサブクラス化した場合は、カスタムデザイナ(?)だかカスタムエディタ(?)を指定しないといけないだら、良く分かりません。おまけに、カスタム何ちゃらを作ってしまったら、標準の「列の編集」が効かなくなるんじゃないの?
とか、悶々、悶々。。。


そうこうのた打っているうちに、ふと…
ColumnHeader コレクションエディターなんてエラそーな名前が付いてるけど、所詮はプロパティエディタじゃないの?
だったら、ListView.Columnsの中身にColumnHeaderEx型のインスタンスが入っていれば、件のエディタ上にカスタムプロパティも一緒に表示されるのではっ?

と思ったら、後は実験あるのみっ!


取り敢えず、ColumnHeaderのサブクラスを宣言。

前にも出た奴
public class ColumnHeaderEx : ColumnHeader {
	public ColumnHeaderEx() : base() {
	}

	[Browsable(true)]
	public bool Editable { get; set; }
}

で、Form1.Designer.csを強引に編集。

Form1.Designer.csの一部(順不同)
            
private System.Windows.Forms.ListView listView1;
private ColumnHeaderEx columnHeader1;    <=    ここを修正
private ColumnHeaderEx columnHeader2;    <=    ここを修正
private ColumnHeaderEx columnHeader3;    <=    ここを修正
            
this.listView1 = new ListView();
this.columnHeader1 = new ColumnHeaderEx();    <=    ここを修正
this.columnHeader2 = new ColumnHeaderEx();    <=    ここを修正
this.columnHeader3 = new ColumnHeaderEx();    <=    ここを修正
            

一旦ビルドしたのち、デザイナから「列の編集」を選択。
出たっ!  Editable

image

よしよし、方向性は間違っていないと見た!

問題

すっごい前が開けた様な気はするんですが、実はまだまだ問題は山積しています。

  • Designer.csを直接編集しちゃうのはダメでしょう
  • 上のエディタ画面でメンバーを追加するとColumnHeaderクラスが追加されちゃいます
  • 相変わらずEditableプロパティの参照にキャストが必要です

でも実はこれら、全部根っこは一緒です。
つまりListView.Columnsの型がColumnHeaderExCollecctionであれば、キャストはいらなくなります。こうなればきっと、プロパティエディタ君はよしなに取り計らってくれて、ジェネレータ氏もDesigner.csのコード生成をうまい事処理してくれるんじゃないかなぁ?(希望的観測)
となれば、コードの直接編集もいらない筈です。

後は、参考になりそうな情報を探すだけ…(何か振出しに戻ってるし)



で、ついに見つけました

残念ながら日本語でも英語でもないので、どんなやり取りがなされているのか(私には)微塵も分かりませんが、サンプルコードは(一揃い纏まってるという意味で)完璧です。
上のページの一番最後のスレッドにダウンロードリンクがあります。そのzipファイルの中にListViewExBarebone.zipってなファイルがあって、それが名前の通りColumnHeaderのサブクラス化の基本骨格だけにシェイプしたコード群です。でもやっぱり若干の贅肉は付いてますが…(多分骨格に対する視点が違うんだと思います)

ただ一つ問題が解決されていません。
このBarebone、やっぱりカスタムデザイナを利用しています。多分この辺りかと…

ListViewEx.cs
        
[Designer(typeof(ListViewExDesigner))]        <==    ここ!
public partial class ListViewEx : ListView {
        

では、実際に確認してみましょう。

image

危惧した通り、「列の編集」選択肢が出てきませんね。
列を追加するには、

imageここから「…」をクリックすれば

image

おー、見慣れた奴だ。Testカスタムプロパティも追加されてますね。
でもでも、ColumnsNewだとぉ?
なんじゃそら。

構造

取り敢えずBareboneから以下を読み取りました。(多分、こんな感じです)

IMG_0153.PNG

  • ListViewのサブクラスとしてListViewExが被さっています
  • ListViewExListViewColumnsプロパティをオーバライドしてIDEから表面上消します
  • 新たなプロパティとしてColumnsNewを定義しています
    こっちがデフォルトプロパティになっています
  • (上の絵では書ききれていませんが)プロパティColumnsColumnsNewは同時に更新が行なわれ、内容を同期させています
  • ListViewExにはカスタムデザイナListViewExDesignerが関連付けられています

ソースで云うと重要なのはここら辺。

ListViewEx.cs(部分)
        
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new ColumnHeaderCollection Columns { get { return base.Columns; } }

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ListViewExColumnHeaderCollection ColumnsNew { get { return this.columns; } }
        

何でここの二重管理をしているかと云うと、表向きは利用者にColumnsNewでサブクラス側を公開して、内部向けには既存のColumnsを操作して貰おう、と云う意図でしょうね。

対策

さて、ここで実験の成果が生きてきます。

Malfuncさんはあくまでも、内部的にはColumnHeaderCollection型のColumnsを使って貰いたいと思っている様ですが、プロパティエディタとしてはそこの型は(余り)気にしていない模様。あくまでもそこに入っているオブジェクトが持っているプロパティを全部見せてくれるだけです。

だからきっと、ColumnsColumnHeaderのサブクラスを格納するコレクションである限り、何も破綻しないのではないだろうか?
どう?
そんな気がしませんか?

となれば、前出の二重プロパティは次の様な形でも大丈夫な筈です。

ListViewEx.cs(部分)
        
[Designer(typeof(ListViewExDesigner))]        <==    この行は削除して下さい
        
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
internal ColumnHeaderCollection ColumnsOrg { get { return base.Columns; } }

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new ListViewExColumnHeaderCollection Columns { get { return this.columns; } }
        

ここでは、表向きはサブクラス化されたColumns、内部で使って貰う方はbase.Columns=ColumnsOrgに分かれています。で、内部向けの方はinternal指定にして、公開プロパティにならない様に気を使っています。

まぁこの入り口だけ修正してもダメで、同期更新の部分もそれなりに手を加えないといけません。
えー、例えばこんな感じ…

ListViewExColumnHeaderCollection.cs(部分) 
        
protected override void OnClearComplete() {
	Validate("OnClearComplete");
	base.OnClearComplete();
	this.listviewex.ColumnsOrg.Clear();
}
        

結果

取り敢えず一通り修正して、検証。
お、出た。

image

image

勝った…

仕上

さて、目的を達成するための部品は揃った様です。最後の仕上げとしてBareboneから贅肉落とした上で本稿の目的であるEditableを実装しましょう。
更に前稿の成果である直接編集ちっくListViewExとも合体させちゃうと、サブクラス化した甲斐があると云うものです。

では―
まずは、ヘッダのサブクラス周りから…

ColumnHeaderEx.cs
using System.ComponentModel;
using System.Windows.Forms;

public class ColumnHeaderEx : ColumnHeader {
	public ColumnHeaderEx() : base() {
		this.Editable = false;
	}

	[Category("Behavior")]
	[Browsable(true)]
	[EditorBrowsable(EditorBrowsableState.Always)]
	[DefaultValue(false)]
	public bool Editable { get; set; }
}
ColumnHeaderExCollection.cs
using System;
using System.Collections;
using System.Windows.Forms;

public class ColumnHeaderExCollection : CollectionBase {
	private ListViewEx owner;

	public ColumnHeaderExCollection(ListViewEx owner) {
		this.owner = owner;
	}

	public void Add(ColumnHeaderEx header) {
		owner.ColumnsOrg.Add((ColumnHeaderEx)header.Clone());
		this.InnerList.Add(header);
	}

	public void AddRange(ColumnHeaderEx[] headers) {
		for(int i = 0; i < headers.Length; i++)
			Add((ColumnHeaderEx)headers[i]);
	}

	public ColumnHeaderEx this[int i] {
		get {
			if(i < 0 || i >= this.InnerList.Count)
				return null;
			return (ColumnHeaderEx)this.InnerList[i];
		}
		set {
			this.InnerList[i] = value;
		}
	}

	protected override void OnInsertComplete(int index, object value) {
		this.owner.ColumnsOrg.Insert(index, (ColumnHeader)((ICloneable)value).Clone());
	}

	protected override void OnRemoveComplete(int index, object value) {
		base.OnRemoveComplete(index, value);
		this.owner.ColumnsOrg.RemoveAt(index);
	}

	protected override void OnClearComplete() {
		base.OnClearComplete();
		this.owner.ColumnsOrg.Clear();
	}
}

んー、ListViewEx.csはもう一回全体を載せちゃいます。差分だけ説明しながら…
とも思ったんですが、まどろっこしいので。

ListViewEx.cs
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

public partial class ListViewEx : ListView {
	public ListViewEx() : base() {
		this.View = View.Details;
		this.FullRowSelect = true;
		this.columns = new ColumnHeaderExCollection(this);

		this.EditBox = new TextBox();
		this.EditBox.Parent = this;
		this.EditBox.Visible = false;
		this.EditBox.BorderStyle = BorderStyle.FixedSingle;
		this.EditBox.Leave += EditBox_Leave;
		this.EditBox.KeyPress += EditBox_KeyPress;

		this.SetStyle(ControlStyles.EnableNotifyMessage, true);
	}

	protected override void Dispose(bool disposing) {
		if(disposing) {
			this.EditBox.KeyPress -= EditBox_KeyPress;
			this.EditBox.Leave -= EditBox_Leave;
			this.EditBox.Dispose();
		}
		base.Dispose(disposing);
	}

	private void EditBox_Leave(object sender, EventArgs e) {
		CurrentColumn.Text = EditBox.Text;
		EditBox.Visible = false;
	}

	private void EditBox_KeyPress(object sender, KeyPressEventArgs e) {
		switch(e.KeyChar) {
		case (char)Keys.Enter:
			this.Focus();
			e.Handled = true;
			break;

		case (char)Keys.Escape:
			EditBox.Text = CurrentColumn.Text;
			this.Focus();
			e.Handled = true;
			break;
		}
	}



	ColumnHeaderExCollection columns;
	TextBox EditBox;



	[Browsable(false)]
	[EditorBrowsable(EditorBrowsableState.Never)]
	public new View View {
		get { return base.View; }
		set { base.View = View.Details; }
	}

	[Browsable(false)]
	[EditorBrowsable(EditorBrowsableState.Never)]
	public new bool FullRowSelect {
		get { return base.FullRowSelect; }
		set { base.FullRowSelect = true; }
	}

	[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
	[Browsable(false)]
	[EditorBrowsable(EditorBrowsableState.Never)]
	internal ColumnHeaderCollection ColumnsOrg { get { return base.Columns; } }

	[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
	[Browsable(false)]
	[EditorBrowsable(EditorBrowsableState.Never)]
	public new ColumnHeaderExCollection Columns { get { return this.columns; } }

	[Browsable(false)]
	public ListViewItem CurrentRow { get; set; }

	[Browsable(false)]
	public ListViewItem.ListViewSubItem CurrentColumn { get; set; }

	[Browsable(false)]
	public int CurrentRowIndex { get { return (CurrentRow == null) ? -1 : CurrentRow.Index; } }

	[Browsable(false)]
	public int CurrentColumnIndex { get { return (CurrentColumn == null) ? -1 : CurrentRow.SubItems.IndexOf(CurrentColumn); } }
	
	protected override void OnItemCheck(ItemCheckEventArgs ice) {
		if(CurrentColumn != null && CurrentColumnIndex != 0)
			ice.NewValue = ice.CurrentValue;

		base.OnItemCheck(ice);
	}

	protected override void OnMouseDown(MouseEventArgs e) {
		Point loc = this.PointToClient(Cursor.Position);

		CurrentRow = null;
		CurrentColumn = null;

		CurrentRow = this.HitTest(loc).Item;
		if(CurrentRow == null || !CurrentRow.Bounds.Contains(loc))
			CurrentRow = null;
		else {
			CurrentColumn = CurrentRow.GetSubItemAt(loc.X, loc.Y);
			if(CurrentColumn == null || !CurrentColumn.Bounds.Contains(loc))
				CurrentColumn = null;
		}

		base.OnMouseDown(e);
	}

	protected override void OnResize(EventArgs e) {
		this.Focus();
		base.OnResize(e);
	}

	protected override void OnColumnWidthChanging(ColumnWidthChangingEventArgs e) {
		this.Focus();
		base.OnColumnWidthChanging(e);
	}

	protected override void OnNotifyMessage(Message m) {
		switch(m.Msg) {
		case WM_HSCROLL:
		case WM_VSCROLL:
			this.Focus();
			break;
		}
		base.OnNotifyMessage(m);
	}
	
	const int WM_HSCROLL = 0x114;
	const int WM_VSCROLL = 0x115;
	
	public void EditColumn() {
		if(CurrentColumn == null || CurrentColumnIndex == 0) return;
		if(!this.columns[CurrentColumnIndex].Editable) return;

		Rectangle rect = CurrentColumn.Bounds;
		rect.Intersect(this.ClientRectangle);
		rect.Y -= 1;

		EditBox.Bounds = rect;
		EditBox.Text = CurrentColumn.Text;
		EditBox.Visible = true;
		EditBox.BringToFront();
		EditBox.Focus();
	}
}

で、こんな感じになりました。

image

image

ごく一般的な操作で表示されたColumnHeaderEx コレクション エディターで、Editableプロパティが増えています。columnHeaderEx3Editabletrueにしてみます。


使い方としてはListViewExMouseDoubleClickイベントハンドラに以下を記述するだけです。

private void listViewEx1_MouseDoubleClick(object sender, MouseEventArgs e) {
	listViewEx1.EditColumn();
}

ダブルクリックされたらとにかくListViewEx.EditColumn()を呼んじゃって下さい。編集対象かどうかはListViewExが判断してくれます。(ま、このインタフェースが良いかどうかは議論の余地があると思いますが…)


ColumnHeaderEx3のカラムはダブルクリックで直接編集ちっくモードに入りました。

image

でも、ColumnHeaderEx2ColumnHeaderEx4のカラムはダブルクリックしても反応しません。

image

ぐー
完成!


後は、実用に耐えられる様にプロパティとか公開メソッドとか真面目に考えないといけないなぁ。
ま、技術的な面では一通り検証できたという事で…

どっとはらい

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?