HTMLのテーブルの行や列を固定化したいことがあると思います。パッケージを購入する方法もあると思いますが、レガシのWebアプリで作成したtableに対して固定化機能を提供したい場合もあると思います。

掲載するプログラムの量が多いので、本記事と、テーブル行ヘッダ列ヘッダ固定化Extender バージョン1.0 その2の2つに解説が分かれています。

今回はASP.NET AJAX 3.5 で固定化を行うFreezeTableExtender を作成する方法を紹介します。ソリューションも公開しているので、ASP.NET AJAX 1.0 でも同じように実装できると思います。

Extender により提供される機能

  • 行ヘッダ固定化
  • 列ヘッダ固定化
  • 列ヘッダセルダブルクリック時に文字幅までセル幅を拡張する
  • 行のリサイズ
  • 列のリサイズ

テーブル行ヘッダ列ヘッダ固定化Extender バージョン1.0 その2 に動作確認用のテストプロジェクトつきのソリューションファイルを公開していますのでよかったら使ってみてください。

動作確認環境

  • 実装環境:ASP.NET AJAX 3.5
  •  動作確認: IE 7

Extender の動作結果のサンプルを作成します。順に、固定化されたテーブルヘッダの初期表示、スクロール後、リサイズ後です。

1. Extender の作成

1.1 プロジェクトの作成

Visual Studio 2008 を起動し、.NET 3.5 をターゲットとして ASP.NET AJAX サーバコントロール エクステンダー テンプレートとして、 ComponentGeek.Extender プロジェクトを作成します。

1.2 FreezeHeaderExtender の作成

既定で作成されるファイルの名前を修正して、FreezeHeaderBehavior.js, FreezeHeaderBehavior.resx, FreezeHeaderExtender.cs というファイル名に変更します。

今回は固定化された行列ヘッダをリサイズできるようにするため、TableResizeBehavior.js という javascript ファイルと、TableResizeBehavior.resx というリソースファイルをプロジェクトに追加します。 TableResizeBehavior.js はビルドアクションで 埋め込まれたリソースに選択します。

1.3 FreezeHeaderExtender.cs の編集

FreezeHeaderExtender.cs を開いて次のように編集します。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using System.ComponentModel;

namespace ComponentGeek.Extender
{
    /// <summary>
    /// FreezeHeaderExtender 
    /// </summary>
    [TargetControlType(typeof(Control))]
    public class FreezeHeaderExtender : ExtenderControl
    {
        public FreezeHeaderExtender() { }

        /// <summary>   
        /// ヘッダテーブルのCssClass   
        /// </summary>   
        public string HeaderCssClass { get; set; }
        /// <summary>   
        /// ボディテーブルのCssClass   
        /// </summary>   
        public string BodyCssClass { get; set; }
        /// <summary>   
        /// ボディ部の幅   
        /// </summary>   
        public Unit Width { get; set; }
        /// <summary>   
        /// ボディ部の高さ   
        /// </summary>   
        public Unit Height { get; set; }
        private FreezeHeaderAdjustWidthMode _widthAdjustMode = FreezeHeaderAdjustWidthMode.Header;
        /// <summary>   
        /// 幅調整モード   
        /// </summary>   
        [DefaultValue(FreezeHeaderAdjustWidthMode.Header)]
        public FreezeHeaderAdjustWidthMode WidthAdjustMode
        {
            get { return _widthAdjustMode; }
            set { _widthAdjustMode = value; }
        }
        public int _headerColumnCount = 0;
        /// <summary>   
        /// ヘッダ列の数   
        /// </summary>   
        [DefaultValue(0)]
        public int HeaderColumnCount
        {
            get { return _headerColumnCount; }
            set { _headerColumnCount = value; }
        }
        public int _headerRowCount = 1;
        /// <summary>   
        /// ヘッダ行の行数   
        /// </summary>   
        [DefaultValue(1)]
        public int HeaderRowCount
        {
            get { return _headerRowCount; }
            set { _headerRowCount = value; }
        }

        protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(System.Web.UI.Control targetControl)
        {
            if (!targetControl.HasControls()) return new ScriptBehaviorDescriptor[] { };

            ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("ComponentGeek.Extender.FreezeHeaderBehavior", targetControl.ClientID);
            if (!string.IsNullOrEmpty(HeaderCssClass))
            {
                descriptor.AddProperty("headerCssClass", HeaderCssClass);
            }
            if (!string.IsNullOrEmpty(BodyCssClass))
            {
                descriptor.AddProperty("bodyCssClass", BodyCssClass);
            }
            if (Height != null)
            {
                descriptor.AddProperty("height", Height.ToString());
            }
            if (Width != null)
            {
                descriptor.AddProperty("width", Width.ToString());
            }
            descriptor.AddProperty("widthAdjustMode", WidthAdjustMode);
            descriptor.AddProperty("headerColumnCount", HeaderColumnCount);
            descriptor.AddProperty("headerRowCount", HeaderRowCount);

            return new ScriptBehaviorDescriptor[] { descriptor };
        }

        // スクリプト参照を生成します
        protected override IEnumerable<ScriptReference> GetScriptReferences()
        {
            yield return new ScriptReference("ComponentGeek.Extender.FreezeHeaderBehavior.js", this.GetType().Assembly.FullName);
            yield return new ScriptReference("ComponentGeek.Extender.TableResizeBehavior.js", this.GetType().Assembly.FullName);
        }
    }
    /// <summary>   
    /// 幅調整モード   
    /// - Default : 幅の広いほうに合わせる   
    /// - Header  : ヘッダ行の幅に合わせる   
    /// - Data    : データ行の幅に合わせる   
    /// </summary>   
    public enum FreezeHeaderAdjustWidthMode
    {
        Default = 0,
        Header,
        Data
    }
}

1.4 FreezeHeaderBehavior.js の編集

FreezeHeaderBehavior.js を編集します。ビルドアクションが埋め込まれたリソースとなっていることを確認して下さい。このエクステンダーで、テーブルを行列のヘッダTableとスクロールするTableにGridView, テーブルを構築しています。

/// <reference name="MicrosoftAjax.js"/>

Type.registerNamespace("ComponentGeek.Extender");

ComponentGeek.Extender.FreezeHeaderBehavior = function(element) {
    ComponentGeek.Extender.FreezeHeaderBehavior.initializeBase(this, [element]);

    this._headerCssClass = null;       // ヘッダのCSSクラス   
    this._bodyCssClass = null;         // データのCSSクラス   
    this._headerDiv = null;            // ヘッダのdiv   
    this._headerTable = null;          // ヘッダのtable   
    this._headerRowCount = 1;          // 行ヘッダ数   
    this._bodyDiv = null;              // データのdiv   
    this._bodyTable = null;            // データのtable   
    this._columnHeaderDiv = null;      // 列ヘッダのヘッダのdiv   
    this._columnHeaderTable = null;    // 列ヘッダのヘッダのtable   
    this._columnHeaderRowDiv = null;   // 列ヘッダのデータのdiv   
    this._columnHeaderRowTable = null; // 列ヘッダのデータのtable   
    this._headerColumnCount = 0;       // 列ヘッダ数   
    this._rootDiv = null;              // ルートdiv   
    this._width = 400;                 // 幅のデフォルト   
    this._height = 200;                // 高さのデフォルト   
    this._vScrollBarWidth = 17; // browser depend   
    this._hScrollBarHeight = 17; // browser depend   
    this._widthAdjustMode = ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.Default; 
}

ComponentGeek.Extender.FreezeHeaderBehavior.prototype = {
    initialize: function() {
        ComponentGeek.Extender.FreezeHeaderBehavior.callBaseMethod(this, 'initialize');

        // カスタム初期化処理   
        this.createFreezeTable();
        this.adjustColWidth();
        this.adjustRowHeight();

        $addHandler(this._bodyDiv, "scroll", Function.createDelegate(this, this._onScroll));

        if (this._headerTable) {
            $create(ComponentGeek.Extender.TableXResizeBehavior, { "header": this._headerTable, "body": this._bodyTable }, null, null, this._headerTable);
        }
        if (this._columnHeaderTable) {
            $create(ComponentGeek.Extender.TableXResizeBehavior, { "header": this._columnHeaderTable, "body": this._columnHeaderRowTable }, null, null, this._columnHeaderTable);
        }
        if (this._columnHeaderTable) {
            $create(ComponentGeek.Extender.TableYResizeBehavior, { "header": this._columnHeaderTable, "body": this._headerTable }, null, null, this._columnHeaderTable);
        }
        if (this._columnHeaderRowTable) {
            $create(ComponentGeek.Extender.TableYResizeBehavior, { "header": this._columnHeaderRowTable, "body": this._bodyTable }, null, null, this._columnHeaderRowTable);
        }
    },
    dispose: function() {
        // カスタムDispose処理
        if (this._bodyDiv) {
            $clearHandlers(this._bodyDiv);
        }
        ComponentGeek.Extender.FreezeHeaderBehavior.callBaseMethod(this, 'dispose');
    },

    get_headerCssClass: function() {
        return this._headerCssClass;
    },
    set_headerCssClass: function(value) {
        this._headerCssClass = value;
    },
    get_bodyCssClass: function() {
        return this._bodyCssClass;
    },
    set_bodyCssClass: function(value) {
        this._bodyCssClass = value;
    },
    get_width: function() {
        return this._width;
    },
    set_width: function(value) {
        this._width = value;
    },
    get_height: function() {
        return this._height;
    },
    set_height: function(value) {
        this._height = value;
    },
    get_vScrollBarWidth: function() {
        return this._vScrollBarWidth;
    },
    set_vScrollBarWidth: function(value) {
        this._vScrollBarWidth = value;
    },
    get_widthAdjustMode: function() {
        return this._widthAdjustMode;
    },
    set_widthAdjustMode: function(value) {
        this._widthAdjustMode = value;
    },
    get_headerColumnCount: function() {
        return this._headerColumnCount;
    },
    set_headerColumnCount: function(value) {
        this._headerColumnCount = value;
    },
    get_headerRowCount: function() {
        return this._headerRowCount;
    },
    set_headerRowCount: function(value) {
        this._headerRowCount = value;
    },
    _onScroll: function(e) {
        this._headerDiv.scrollLeft = this._bodyDiv.scrollLeft;
        if (this._columnHeaderRowDiv) {
            this._columnHeaderRowDiv.scrollTop = this._bodyDiv.scrollTop;
        }
    },
    _showIfDisplayNone: function() {
        if (this._headerTable && this._headerTable.style.display == "none")
            this._headerTable.style.display = "";

        if (this._bodyTable && this._bodyTable.style.display == "none")
            this._bodyTable.style.display = "";

        if (this._columnHeaderTable && this._columnHeaderTable.style.display == "none")
            this._columnHeaderTable.style.display = "";

        if (this._columnHeaderRowTable && this._columnHeaderRowTable.style.display == "none")
            this._columnHeaderRowTable.style.display = "";
    },
    populateColumnHeader: ComponentGeek$Extender$FreezeHeaderBehavior$populateColumnHeader,
    populateColumnHeaderRow: ComponentGeek$Extender$FreezeHeaderBehavior$populateColumnHeaderRow,
    populateHeader: ComponentGeek$Extender$FreezeHeaderBehavior$populateHeader,
    populateBody: ComponentGeek$Extender$FreezeHeaderBehavior$populateBody,
    createFreezeTable: ComponentGeek$Extender$FreezeHeaderBehavior$createFreezeTable,
    adjustColWidth: ComponentGeek$Extender$FreezeHeaderBehavior$adjustColWidth,
    adjustRowHeight: ComponentGeek$Extender$FreezeHeaderBehavior$adjustRowHeight
}

function ComponentGeek$Extender$FreezeHeaderBehavior$createFreezeTable(){   
    // テーブルを作成する   
    var table;   
    if(this.get_element().tagName == "TABLE") table = this.get_element();   
    else{   
        var tables = get_element().getElementByTagName("table");   
        if(tables.length == 0) throw Error.argument('element', 'table is not found');   
           
        table = tables[0];   
    }   
    this._rootDiv = document.createElement("div");   
    table.parentElement.insertBefore(this._rootDiv, table);   
    this.populateHeader(table);   
    this.populateBody(table);   
    this.populateColumnHeader();   
    this.populateColumnHeaderRow();   
    // div直下にさらにtableを作成して追加   
    var t = document.createElement("table");   
    t.cellSpacing = 0;   
    t.cellPadding = 0;   
    t.border = 0;   
    var b = document.createElement("tbody");   
    t.appendChild(b);   
    var r = document.createElement("tr");   
    var d = document.createElement("td");   
    r.appendChild(d);   
    if(this._columnHeaderDiv){   
        d.appendChild(this._columnHeaderDiv);   
        d = document.createElement("td");   
        r.appendChild(d);   
    }   
    d.appendChild(this._headerDiv);   
    b.appendChild(r);   
                   
    r = document.createElement("tr");   
    d = document.createElement("td");   
    r.appendChild(d);   
    if(this._columnHeaderRowDiv){   
        d.appendChild(this._columnHeaderRowDiv);   
        d = document.createElement("td");   
        r.appendChild(d);   
    }   
    d.appendChild(this._bodyDiv);   
    b.appendChild(r);   
       
    this._rootDiv.appendChild(t);   
  
    this._showIfDisplayNone();   
    // 構築結果の出力   
    // デバッグ情報を出力したい場合はid=traceConsoleのtextareaを作成すること   
    Sys.Debug.traceDump(this._rootDiv.outerHTML, "ComponentGeek.FreezeHeaderBehavior OuterHtml");   
    Sys.Debug.traceDump(this, "ComponentGeek.FreezeHeaderBehavior trace");   
}   
function ComponentGeek$Extender$FreezeHeaderBehavior$populateHeader(orgTable){   
    // create header div   
    this._headerDiv = document.createElement("div");   
    this._headerDiv.style.width = this._width;   
    this._headerDiv.style.overflow = "hidden";   
    this._headerDiv.style.position = "relative";   
    // create header table
    this._headerTable = orgTable.cloneNode();
    this._headerTable.id = this._headerTable.id + "_RowHeader";
    this._headerTable.style.height = "";  // 高さは引き継がない   
    if(this._headerCssClass && this._headerCssClass.length > 0){    
        this._headerTable.className = this._headerCssClass;           
    }   
    this._headerTable.style.tableLayout  = "fixed";   
    this._headerTable.style.marginRight = this._vScrollBarWidth;   
  
    // create header content   
    var headerBody = document.createElement("tbody");   
    if(this._headerRowCount > 0 && orgTable.rows.length >= this._headerRowCount){   
        for(var i=0;i<this._headerRowCount;++i){   
            var headerRow = orgTable.rows(0);   
            headerBody.appendChild(headerRow);   
        }
    }   
    this._headerTable.appendChild(headerBody);   
    this._headerDiv.appendChild(this._headerTable);   
}   
function ComponentGeek$Extender$FreezeHeaderBehavior$populateBody(bodyTable){   
    // create body div   
    this._bodyDiv = document.createElement("div");   
    this._bodyDiv.style.height = this._height;   
    this._bodyDiv.style.width = this._width;   
    this._bodyDiv.style.overflow = "scroll";   
    this._bodyDiv.appendChild(bodyTable);   
    this._bodyTable = bodyTable;   
    this._bodyTable.style.tableLayout  = "fixed";   
    if(this._bodyCssClass && this._bodyCssClass.length > 0){    
        this._bodyTable.className = this._bodyCssClass;           
    }   
}   
function ComponentGeek$Extender$FreezeHeaderBehavior$adjustColWidth(){   
    /// <summary>列幅を一致させる</summary>   
    if(this._headerTable.rows.length == 0 || this._bodyTable.rows.length == 0) return;   
    var row = this._headerTable.rows(0); // ヘッダ   
    for(var i=0;i< row.cells.length;++i){   
        var hw = row.cells(i).offsetWidth;   
        var bw = this._bodyTable.rows(0).cells(i).offsetWidth;   
        if(hw > bw){   
            this._bodyTable.rows(0).cells(i).style.width = hw;   
            row.cells(i).style.width = hw;   
        }else{   
            this._bodyTable.rows(0).cells(i).style.width = bw;   
            row.cells(i).style.width = bw;   
        }   
    }   
    if(this._columnHeaderTable && this._columnHeaderRowTable){   
        var hrow = this._columnHeaderTable.rows(0); // ヘッダ   
        var drow = this._columnHeaderRowTable.rows(0);   
        for(var i=0;i< hrow.cells.length;++i){   
            var hw = hrow.cells(i).offsetWidth;   
            var dw = drow.cells(i).offsetWidth;   
            var w = "auto";   
            if(this._widthAdjustMode == ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.Default){   
                if(hw > dw){   
                    w = hw;   
                }else{   
                    w = dw;   
                }   
            }else if(this._widthAdjustMode == ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.Header){   
                w = hw;   
            }else if(this._widthAdjustMode == ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.Data){   
                w = dw;   
            }   
            hrow.cells(i).style.width = w;         
            drow.cells(i).style.width = w;               
        }           
    }   
}   
function ComponentGeek$Extender$FreezeHeaderBehavior$adjustRowHeight(){   
    /// <summary>ヘッダの高さを大きいほうに一致させる</summary>
    if (this._columnHeaderTable && this._headerTable) {
        if (this._columnHeaderTable.rows.length == 0) return; // 行がない場合調整しない
        var leftRow = this._columnHeaderTable.rows(0);
        var rightRow = this._headerTable.rows(0);   
        var lh = leftRow.offsetHeight;   
        var rh = rightRow.offsetHeight;   
        if(lh > rh){   
            leftRow.style.height = lh;   
            rightRow.style.height = lh;   
        }else{   
            leftRow.style.height = rh;   
            rightRow.style.height = rh;   
        }   
    }   
}   
function ComponentGeek$Extender$FreezeHeaderBehavior$populateColumnHeader(){   
    if(!this._headerTable) return;   
    if(this._headerColumnCount == 0) return;   
    // create column header   
    this._columnHeaderDiv = document.createElement("div");
    this._columnHeaderTable = this._headerTable.cloneNode(false);
    var headerBody = document.createElement("tbody");   
    this._columnHeaderTable.appendChild(headerBody);   
       
    for(var i=0;i<this._headerTable.rows.length;++i){   
        var newRow = this._headerTable.rows(i).cloneNode(false);   
        headerBody.insertBefore(newRow);   
        //var newRow = headerBody.insertRow(i);   
        var orgRow = this._headerTable.rows(i);   
        for(var j = 0;j < this._headerColumnCount;++j){   
            var cell = orgRow.cells(0);   
            newRow.appendChild(cell);   
        }   
    }   
    this._columnHeaderTable.style.marginRight = 0;
    this._columnHeaderTable.style.tableLayout = "fixed";
    this._columnHeaderTable.id = this._columnHeaderTable.id + "_ColumnHeader";

    this._columnHeaderDiv.appendChild(this._columnHeaderTable);       
    this._columnHeaderDiv.style.position = "relative";   
}
function ComponentGeek$Extender$FreezeHeaderBehavior$populateColumnHeaderRow() {
    if (!this._bodyTable) return;
    if (this._headerColumnCount == 0) return;
    // create column header   
    this._columnHeaderRowDiv = document.createElement("div");
    this._columnHeaderRowTable = this._bodyTable.cloneNode(false);
    var headerBody = document.createElement("tbody");
    this._columnHeaderRowTable.appendChild(headerBody);

    for (var i = 0; i < this._bodyTable.rows.length; ++i) {
        var newRow = this._bodyTable.rows(i).cloneNode(false);
        headerBody.insertBefore(newRow);
        var orgRow = this._bodyTable.rows(i);

        for (var j = 0; j < this._headerColumnCount; ++j) {
            var cell = orgRow.cells(0);
            newRow.appendChild(cell);
        }
    }

    this._columnHeaderRowDiv.appendChild(this._columnHeaderRowTable);
    this._columnHeaderRowDiv.style.height = this._height;
    this._columnHeaderRowDiv.style.overflow = "hidden";
    this._columnHeaderRowDiv.style.position = "relative";

    this._columnHeaderRowTable.style.tableLayout = "fixed";
    this._columnHeaderRowTable.style.marginBottom = this._hScrollBarHeight;
    this._columnHeaderRowTable.style.marginRight = 0;
    this._columnHeaderRowTable.id = this._columnHeaderRowTable.id + "_ColumnHeaderRow";
    if (this._bodyCssClass && this._bodyCssClass.length > 0) {
        this._columnHeaderRowTable.className = this._bodyCssClass;
    }
}

ComponentGeek.Extender.FreezeHeaderBehavior.registerClass('ComponentGeek.Extender.FreezeHeaderBehavior', Sys.UI.Behavior);

ComponentGeek.Extender.FreezeHeaderAdjustWidthMode = function() { };
ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.prototype =
{
    Default: 0,  // サイズの大きいほうに合わせる   
    Header: 1,  // ヘッダ行の幅に合わせる   
    Data: 2   // データ行の1行目の幅に合わせる   
}
ComponentGeek.Extender.FreezeHeaderAdjustWidthMode.registerEnum("ComponentGeek.Extender.FreezeHeaderAdjustWidthMode");   

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

とりあえず今回はここまでです。続きはテーブル行ヘッダ列ヘッダ固定化Extender バージョン1.0 その2を参照してください。リンク先にはソースつきのソリューションも公開指定あります。