반응형

지금까지 Grid Control의 Header 부분과 가로 ScrollBar까지의 기능을 만들어 보았다.

추가한 Context Menu 중 "행 추가" 기능을 만들어서 Grid Control에서 행(Row)를 추가해 보기로 하자.

[그림 9-1] 행 추가

행(Row) 추가를 하게 되면 Grid Control의 마지막 행(Row) 뒤에 새로운 행(Row)를 추가하도록 한다.

한 개의 행에 대한 컬럼들은 Header 설정 시에 각 컬럼별로 폭(Width), 컬럼 ID, 정렬방법 등을 설정했기에 각 컬럼의 데이터를 저장하기 위해서 DataTable을 이용하기로 한다.

실제 사용 시에도 Database에서 가져온 데이터를 Grid Control에서 Display할 경우 Datatable 타입으로 할당해서 사용하는 것이 일반적이기에 Grid Control 내에서 추가되는 모든 행(Row)은 DataTable에 저장하기로 한다.

Header 설정 시 지정한 Column ID가 Datatable의 Column ID로 사용되어진다.

 

WANIGrid에서 행(Row) 추가 시에 생성되는 Row Class를 먼저 살펴보자.

1개의 행에 대해서 관리되어져야 할 정보는

  • 행(Row)의 Background Color - 행(Row) 선택 시 백그라운드 색상
  • 행(Row)의 Foreground Color 
  • 행(Row) 선택 여부
  • 행(Row) 데이터의 최대 라인수 - 기본 1라인
  • 행(Row)에 해당하는 컬럼 정보를 담고 있는 DataRow

한 개의 행(Row) 정보를 관리하기 위한 Row Class의 속성과 메소드를 살펴보자.

    public class Row : ICloneable
    {
        #region 변수
        private Color backColor;
        private Color foreColor;
        private bool selected;
        private int maxLines;
        private DataRow row;
        #endregion 변수

        #region 생성자
        public Row()
        {
            maxLines = 1; //maxLines는 기본 1라인
            selected = false;
            foreColor = Color.Black;
            backColor = Color.LightGray;
        }

        public Row(DataRow dRow)
        {
            maxLines = 1;
            selected = false;
            foreColor = Color.Black;
            backColor = Color.LightGray;
            row = dRow;
        }
        #endregion 생성자

        #region Properties
        public int MaxLines
        {
            get { return maxLines; }
            set
            {
                if (maxLines != value) maxLines = value;
            }
        }

        public Color BackColor
        {
            get { return backColor; }
            set { backColor = value; }
        }

        public Color ForeColor
        {
            get { return foreColor; }
            set { foreColor = value; }
        }

        public bool Selected
        {
            get { return selected; }
            set { selected = value; }
        }

        public DataRow DataRow
        {
            get { return row; }
            set { row = value; }
        }
        #endregion Properties

        #region Method
        public object Clone()
        {
            Row row = new Row();
            row.BackColor = backColor;
            row.ForeColor = foreColor;
            row.Selected = selected;
            return row;
        }
        #endregion Method
    }

 

행(Row) 추가 시 생성된 Row Class에 대한 관리가 필요하며, 이러한 관리를 위해서 RowCollection Class를 생성한다.

주요 기능으로는

  • Index 위치의 Row 정보를 설정하거나 가져온다
  • 특정 Row 정보가 존재하는 Index 반환
  • Row를 추가하고 Index를 반환
  • 여러 개의 Row를 추가
  • 특정 Row를 제거
  • 모든 Row를 Clear
  • Row의 Index 반환
  • 특정 Index 위치에 Row를 Insert

    public class RowCollection: CollectionBase
    {
        /// index 위치의 Row 정보를 설정하거나 가져온다.
        public Row this[int index]
        {
            get
            {
                if (index >= 0 && index < this.Count)
                    return List[index] as Row;
                else
                    return null;
            }
            set
            {
                List[index] = value;
            }
        }

        /// Row 정보가 존재하는 Index를 반환한다.
        public int this[Row item]
        {
            get { return List.IndexOf(item); }
        }

        /// Row를 추가하고 추가한 Row의 Index를 반환
        public virtual int Add(Row item)
        {
            int index = List.Add(item);
            return index;
        }

        /// 여러 개의 Row를 추가한다.
        public virtual void AddRange(Row[] items)
        {
            lock(List.SyncRoot)
            {
                for (int i = 0; i < items.Length; i++)
                {
                    List.Add(items[i]);
                }
            }
        }

        /// Row를 제거한다.
        public virtual void Remove(Row item)
        {
            List.Remove(item);
        }

        /// 모든 Row를 지운다.
        public new void Clear()
        {
            List.Clear();
        }

        /// Row의 Index를 반환  
        public virtual int IndexOf(Row item)
        {
            return List.IndexOf(item);
        }

        /// 특정 Index 위치에 Row를 Insert 한다.
        public virtual void Insert(int index, Row item)
        {
            List.Insert(index, item);
        }
    }

 

행(Row) 추가 시 다루어져야 할 정보에 대해서 알아 보았다. 행과 관련된 Row Class, RowCollection Class를 이용해서 추가 된 행(Row)을 WANIGrid Control에 나타내고(Display), 추가된 행(Row)의 컬럼 정보들을 관리하게 된다.

각 행(Row)에 대한 정보는 Row Class를 이용해서 관리하며, 행(Row) 전체에 대한 관리는 RowCollection Class를 이용해서 관리하게 될 것이다.

추가된 행(Row) 정보를 근간으로 사용자에게 시각적으로 행(Row)이 추가되었음을 보여줄 수 있도록 처리하기 위한 로직을 WINIGrid Control에 추가할 것이다.

 

WINIGrid Control에 추가되는 멤버 변수는

    private int firstVisibleRow = 0;    //화면상에서 처음 보여져야할 로우
    private int lastVisibleRow = 0;     //화면상에서 마지막에 보여질 로우

    private int allRowsHeight = 0;  //Grid Row의 전체 높이 

    private int rowHeight = 0;  //Row 높이
    private RowCollection rows;   //Row 정보를 관리하게 되는 Collection Class
    private DataTable dataSource;  //하나의 Row에 속하는 컬럼 정보를 저장하기 위한 DataTable 

 

dataSource 멤버 변수와 연계되는 프로퍼티 DataSource를 정의

    public DataTable DataSource
    {
        get { return dataSource; }
        set { dataSource = value; }
    }

WINIGrid Control의 Header Font와 Content Font를 정의하기 위한 프로퍼티 추가

    [Category("Font"), Description("The header font")]
    public Font HeaderFont
    {
        get { return headerFont; }
        set
        {
            if (headerFont != value)
            {
                headerFont = value;
                topHeaderHeight = headerFont.Height + 4;
            }
        }
    }
    [Category("Font"), Description("The Content font")]
    public new Font Font
    {
        get { return contentFont; }
        set
        {
            if (contentFont != value)
            {
                contentFont = value;
                rowHeight = Font.Height + 4;
            }
        }
    }

 

WINIGrid Control의 생성자에 RowCollection 멤버 변수 생성 및 HScrollBar/VScrollBar 설정을 추가한다.

public WANIGrid()

{

    .....

    rows = new RowCollection();  //RowCollection 생성
    hScrollBar.SmallChange = 1;   //HScrollBar의 SmallChange 값을 1로 설정
    rowHeight = Font.Height + 4;   //추가되는 Row의 높이 설정
    vScrollBar.SmallChange = rowHeight;  //VScrollBar의 SmallChange 값을 Row의 높이로 설정

    .....

}

 

Context Menu 초기화 메소드인 InitializeContextMenu()에 "행 추가" 선택 시 발생하는 이벤트 핸들러를 등록한다.

private void InitializeContextMenu()
{
    rightClickMenu = new ContextMenu();
    rightClickMenu.MenuItems.Add(LanguageResource.Row_Before_Insert);
    rightClickMenu.MenuItems.Add(LanguageResource.Row_After_Insert);
    rightClickMenu.MenuItems.Add(LanguageResource.Row_Append);
    rightClickMenu.MenuItems.Add(LanguageResource.Row_Delete);

    rightClickMenu.MenuItems[2].Click += new EventHandler(OnMenu_AppenRow_Click);
}

 

가로/세로 ScrollBar에 대한 Visible, Maximum/LargeChange/SmallChange 등을 설정하는 ReCalcScrollBars() 메소드에 Row의 수에 따라 세로 스크롤바를 보여주는 로직을 추가하도록 한다.

private void ReCalcScrollBars()
{

    .....

    //로우의 높이가 클라이언트 사이즈 높이 보다 클 경우 세로 스크롤바를 보여준다.
    if (allRowsHeight > 0 && (allRowsHeight > Height - topHeaderHeight - xsclHeight))
    {
        vScrollBar.Visible = true;
        vScrollBar.Maximum = allRowsHeight;
        vScrollBar.LargeChange = rowHeight * 5;
        vScrollBar.SmallChange = rowHeight;
    }
    else
    {
        vScrollBar.Visible = false;
        grid.LastVisibleRow = 0;
    }

    .....

}

 

화면에 보여져야 할 영역을 계산하는 함수인 CalcVisibleRange()에서 firstVisibleRow 값을 기준으로 lastVisibleRow를 찾아내는 로직을 추가한다.

private void CalcVisibleRange()
{

    ......

    //보여져야 할 Row 영역 계산
    int tempRow = 0;
    int rowIndex = 0;
    if (rows.Count > 0)
    {
        for (rowIndex = firstVisibleRow, tempRow = 0; rowIndex < rows.Count && 

             tempRow < Height - xsclHeight - topHeaderHeight; rowIndex++)
        {
            tempRow += rows[rowIndex].MaxLines * rowHeight;
        }
        lastVisibleRow = rowIndex;

        if (lastVisibleRow >= rows.Count) lastVisibleRow = rows.Count - 1;
    }
    else
    {
        lastVisibleRow = 0;
    }

    ......

}

 

행 추가 선택 시 행을 추가하기 위한 메소드

public void AppendRow()
{
    DataRow row = dataSource.NewRow(); //1개 행의 컬럼들의 정보를 담기 위한 Row생성
    Row r = new Row(row);  //DataRow 생성
    rows.Add(r); //DataRow 추가
    dataSource.Rows.Add(row); //DataTable에 1개의 행을 추가
    allRowsHeight += Font.Height + 4;  //행이 추가될때 추가된 전체 행 높이를 계산
    rowHeight = Font.Height + 4; //행 1개의 높이 설정
    OnRowsChanged(); //Row변경 시 처리하는 메소드 호출
}

 

public void OnRowsChanged()
{
    ReCalcScrollBars(); //가로세로 스크롤바 설정
    CalcVisibleRange();  //화면에 보여져야 할 영역 계산
    Invalidate(true);
}

 

Context Menu 중 "행 추가"를 선택했을 때 발생하는 Event 처리 메소드

protected void OnMenu_AppenRow_Click(object sender, EventArgs e)
{
    AppendRow();
}

 

WANIGrid의 내용을 그리기 위한 메소드를 작성한다.

/// 
/// Grid의 내용 그리기
/// 
private void DrawContent(Graphics g, Rectangle rc, int controlWidth)
{
    SolidBrush brush = new SolidBrush(SystemColors.ControlLight);
    Pen pen = new Pen(Color.LightGray);

    if (rows.Count <= 0) return;
    g.Clip = new Region(new Rectangle(1, topHeaderHeight, Width - ysclWidth + 2, 

                                                     Height - xsclHeight - topHeaderHeight));
    try
    {
        int columnStartY = topHeaderHeight;
        for (int i = firstVisibleRow; i <= lastVisibleRow && i < rows.Count; i++)
        {
            int columnStartX = 0;
            int columnWidth = 0;

            for (int j = firstVisibleCol; j <= lastVisibleCol && j < grid.GridHeaderList.Count; j++)
            {
                if (j == firstVisibleCol)
                {
                    g.FillRectangle(brush, 1, columnStartY + 1, leftHeaderWidth, rowHeight);
                    g.DrawRectangle(pen, 1, columnStartY + 1, leftHeaderWidth, rowHeight);
                    columnStartX += leftHeaderWidth;     
                }

                //보여지는 컬럼의 폭이 컨트롤의 폭 보다 클경우
                if (columnStartX + grid.GridHeaderList[j].Width > controlWidth)
                {
                    columnWidth = controlWidth - columnStartX - 3;
                    g.DrawRectangle(pen, columnStartX + 1, columnStartY + 1, 

                                             columnWidth, rowHeight);                            
                }
                else
                {
                    columnWidth = grid.GridHeaderList[j].Width;
                    g.DrawRectangle(pen, columnStartX + 1, columnStartY + 1, 

                                            columnWidth, rowHeight);                            
                }
                columnStartX += columnWidth;                        
            }

            columnStartY += rowHeight;
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }
}

 

WANIGrid Control의 Paint 메소드인 WANIGrid_Paint(object sender, PaintEventArgs e) 에 DrawContent 메소드를 추가한다.

private void WANIGrid_Paint(object sender, PaintEventArgs e)
{
    CalcVisibleRange();
    ReCalcScrollBars();
    DrawBackground(e.Graphics, rc);
    DrawHeaders(e.Graphics, rc);
    DrawContent(e.Graphics, rc, this.ClientRectangle.Width);
}

 

Build 및 실행 후 "행 추가"를 선택하면 아래와 같이 행이 추가되는 모습을 직접 WANIGrid Control에서 볼 수 있게 된다.

행의 전체 높이가 WANIGrid Control의 높이 보다 적기에 세로 스크롤바는 현재 보이지 않는다.

[그림 10-2] 행 추가 화면

행을 계속 추가하면 행 전체의 높이가 WANIGrid Control의 높이보다 크게 되면 세로 스크롤바가 나타나게 된다.

[그림 10-3] 전체 행 높이가 크지면 나타나는 세로 스크롤바

WANIGrid Control에서 행을 추가 했을 경우, 사용자에게 추가된 행을 시각적으로 보여주는 방법과 세로 스크롤바를 제어하는 방법을 구현해 보았다.

다음 시간에는 행 추가 후 세로 스크롤바 제어에 대해서 살펴 보기로 하자.

지금 까지의 소스는 아래의 링크를 통해서 다운 받을 수 있다.

WANI Grid_20190709.zip
0.33MB

 

 

 

 

반응형
반응형

지금까지 WANIGrid에 Column Header를 설정하고 생성해서 Grid의 모습으로 보여지게끔 만들었다.

지금까지 만든 Grid 틀에 Row를 생성하기 위한 Interface인 Context Menu를 만들어 보기로 하자.

Control 내의 Mouse 우측 버튼 클릭 시 제공될 Context Menu를 만들기 전에 메뉴와 메시지 등에 대한 다국어 지원을 할 수 있도록 설정하는 방법을 설명하겠다.

현재 WANI Grid 프로젝트 내에 다국어처리를 위한 Resource파일을 관리하기 위한 폴더를 하나 추가한다.

폴더의 명칭은 간단하게 Resources로 한다.

 

WANIGrid Control의 속성 중 다국어 지원을 위해서 Language와 Localizable 속성에 아래와 같이 값을 설정한다.

  • Language - 영어(미국)
  • Localizable - True

[그림 8-1] WANIGrid Control 속성 변경

WANIGrid Control 내에서 마우스 우측 버튼 클릭 시 제공되어야 할 메뉴는 아래와 같이 한글과 영문으로 제공한다.

  • Row.Append - 행 추가 (Append row)
  • Row.After.Insert - 다음 행에 추가 (Insert after this row)
  • Row.Before.Insert - 이전 행에 추가 (Insert before this row)
  • Row.Delete - 행 삭제 (Delete row)

한글과 영문으로 제공하기 위해서 Resource파일들을 WANI Grid 프로젝트의 Resources폴더 아래에 만든다.

만들어진 Resources폴더 선택 후 우측 버튼을 눌러서 [추가] > [새항목]을 선택해서 리소스를 만든다.

[그림 8-2] 리소스 파일 생성

Default 언어를 영어(미국)으로 설정했고, 한글을 추가하는 것으로 했기에 리소스 파일을 LanguageResource.resx와 LanguageResource.ko-KR.resx으로 2개 생성한다.

[그림 8-3] 기본인 영어(미국) 리소스 파일 생성 및 작성
[그림 8-4] 한국어(대한민국) 리소스 파일 생성 및 작성

WINIGrid.cs 파일을 열어서 마우스 우측 버튼 클릭 시 제공되어야 할 ContextMenu를 담기위한 변수를 선언한다.


        private ContextMenu rightClickMenu = null;  //Mouse 우측 버튼 클릭 시 제공되는 메뉴

변수 선언 후 ContextMenu 항목을 생성하기 위한 초기화 메소드를 아래와 같이 생성한다.

        private void InitializeContextMenu()
        {
            //영문 메뉴가 정상적으로 나오는지 확인을 위해서는 아래 주석처리한 2라인을 주석해제해서 테스트 하면 된다.
            //System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US"); // 한국어 : "ko-KR"
            //LanguageResource.Culture = cultureInfo;
            rightClickMenu = new ContextMenu();
            rightClickMenu.MenuItems.Add(LanguageResource.Row_Before_Insert);
            rightClickMenu.MenuItems.Add(LanguageResource.Row_After_Insert);
            rightClickMenu.MenuItems.Add(LanguageResource.Row_Append);
            rightClickMenu.MenuItems.Add(LanguageResource.Row_Delete);
        }

 

WANIGrid Control의 생성자에 위의 ContextMenu 초기화 메소드를 호출하도록 추가한다.

        public WANIGrid()
        {            
            InitializeComponent();
            if (grid == null) grid = new WANI_Grid.Grid.Grid();            
            hScrollBar.SmallChange = 1;
            //마우스 우측 버튼 클릭 시 제공되는 ContextMenu 초기화
            InitializeContextMenu();            
        }        

 

이렇게 컨트롤 내에서 마우스 우측 버튼 클릭 시 제공되는 Context Menu에 대한 설정은 마무리 되었다.

실제 마우스 우측 버튼 클릭 시 발생하는 이벤트를 추가해서 실제 메뉴가 정상적으로 나타나는지 확인하도록 하자.

WANIGrid Control에서 MouseDown 이벤트를 추가하고, Column Header 영역에서 우측 버튼이 클릭 되었을 때는 메뉴가 나타나지 않도록 처리하고 Column Header영역이 아닌 곳에서는 메뉴가 나타나도록 한다.

        private void WANIGrid_MouseDown(object sender, MouseEventArgs e)
        {
            //마우스 우측 버튼 클릭 시 Context 메뉴 제공
            if (e.Button == MouseButtons.Right)
            {
                //Grid Header 영역이 선택되어졌을 경우에는 메뉴 제공하지 않음.
                if (e.Y < grid.TopHeaderHeight) return;
                //마우스 우측 버튼이 클릭된 좌표 값을 얻어온다.
                Point p = new Point(e.X, e.Y);

                rightClickMenu.Show(this, p);
            }
        }

 

소스 추가 후 빌더해서 실행하면 아래와 같이 ContextMenu 가 정상적으로 제공됨을 알 수 있다.

 

[그림 8-5] ContextMenu 한글

ContextMenu가 영어(미국)으로 제공되는지 확인하기 위해서는 InitializeContextMenu 메소드 내의 주석처리 되어져 있는 부분의 주석을 해제하고 실행하면 된다.
            System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US"); // 한국어 : "ko-KR"
            LanguageResource.Culture = cultureInfo;

 

[그림 8-6] ContextMenu 영어

지금까지 설명한 소스는 아래의 첨부 파일을 참조하자.

WANI Grid_20190622.zip
0.30MB

반응형
반응형

앞 시간에 Grid Header를 그렸고 이제는 가로 스크롤바에 대한 이벤트를 생성해보기로 하자.

Column이 많아지면 가로 스크롤바가 나타나고 스크롤 이벤트에 따라 컬럼들을 이동해서 보여주어야 한다.

이러한 작업을 하기 위해서 먼저 DefaultHeaderGenerator Class의 DrawHeaders 메소드를 살펴 보기로 하자.

이전 소스에서는 첫번째 컬럼부터 마지막 컬럼까지 Header를 보여주기 위한 목적으로 DrawHeaders 메소드를 간단하게 작성했었다.

이제는 화면에 보여져야 할 첫번째 컬럼과 마지막 컬럼을 기준으로 Grid Header정보를 그려야 한다.

이전 소스의 변경 부분은 아래와 같다.

[HeaderGenerator.cs]

Header의 첫번째 컬럼에서 마지막 컬럼까지의 폭이 Client 폭 보다 클 경우에 true, 그렇지 않을 경우에 false를 설정하기 위한 변수 선언

protected bool isLargeLastCol = false;    //첫번째 컬럼에서 마지막 컬럼 까지의 폭이 Client 폭 보다 클 경우 true

 

외부에서 get/set 할 수 있도록 Property 선언

public bool IsLargeLastCol
{
    get { return isLargeLastCol; }
    set { isLargeLastCol = value; }
}

 

[DefaultHeaderGenerator.cs]

public override void DrawHeaders(int firstVisibleCol, int lastVisibleCol, int controlWidth, Graphics graphics, Rectangle rect)
{
    SolidBrush brush = new SolidBrush(SystemColors.Control);
    Pen pen = new Pen(SystemColors.ControlLight);

    int columnStartX = 0;
    graphics.FillRectangle(brush, columnStartX + 1, 1, leftHeaderWidth, topHeaderHeight);
    graphics.DrawRectangle(pen, columnStartX + 1, 1, leftHeaderWidth, topHeaderHeight);

    for (int i = firstVisibleCol; i <= lastVisibleCol; i++)
    {
        if (i == firstVisibleCol) columnStartX += leftHeaderWidth;  //첫 시작컬럼의 폭을 leftHeaderWidth 만큼 설정
        int headerWidth = this._headers[i].Width;   //i 번째 컬럼의 폭을 설정

        //보여지는 컬럼의 폭이 컨트롤의 폭 보다 클경우
        if (columnStartX  + headerWidth > controlWidth)
        {
            headerWidth = controlWidth - columnStartX - 3;
            if (lastVisibleCol == _headers.Count - 1) IsLargeLastCol = true;
        } else
        {
            IsLargeLastCol = false;
        }

        //헤더영역의 사각형을 채우고 테두리를 그린다.
        graphics.FillRectangle(brush, columnStartX + 1, 1, headerWidth, topHeaderHeight);
        graphics.DrawRectangle(pen, columnStartX + 1, 1, headerWidth, topHeaderHeight);

        //헤더 타이틀 정렬 방법 설정
        StringFormat sf = new StringFormat();
        sf.LineAlignment = StringAlignment.Center;
        sf.Alignment = StringUtil.GetStringAlignment(sf, HorizontalAlignment.Center);

        //헤더 타이틀을 그린다.
        Rectangle colRec = new Rectangle(columnStartX + 1, 1, headerWidth, topHeaderHeight);
        graphics.DrawString(this._headers[i].Title, headerFont, blackBrush, colRec, sf);

         columnStartX += headerWidth;
    }
}

 

[WANIGrid.cs]

가로 스크롤바를 움직였을 때 동작하기 위한 변수를 추가로 선언한다.

private int allColsWidth = 0;   //Grid 컬럼 전체의 폭
private int lastHScrollValue = 0; //가로 스크롤바를 최대값까지 갔을 경우 보여지는 컬럼 수를 2로 나눈 값 - 스크롤 시 이동 거리를 말함
private bool chkLast = false; //마지막 컬럼의 폭을 지정했던 폭 만큼 보여졌을 때 true

 

생성자에 hScrollBar의 최소 변경 값을 설정한다.

public WANIGrid()
{            
    InitializeComponent();
    if (grid == null) grid = new WANI_Grid.Grid.Grid();             
    hScrollBar.SmallChange = 1;
}

 

이벤트 핸들러 초기화

public void EventHandlerInitialize()
{
    hScrollBar.Scroll += new ScrollEventHandler(HScrollBar_Scroll);
}

 

가로 스크롤바를 스크롤 했을 때 발생하는 이벤트

        public void HScrollBar_Scroll(object sender, ScrollEventArgs e)
        {
            //가로 스크롤바를 움직여서 마지막 컬럼이 Client 영역에 나타났을 때
            if (e.NewValue >= (grid.GridHeaderList.Count - lastHScrollValue))
            {
                //마지막 컬럼의 전체가 Client 영역에 모두 나타나지 않았을 때
                if (grid.HeaderGen.IsLargeLastCol)
                {
                    //마지막 컬럼을 1칸 앞으로 땡긴다.
                    if (grid.LastVisibleCol >= grid.GridHeaderList.Count - 1)
                    {
                        if (!chkLast)
                        {
                            firstVisibleCol += 1;
                            grid.FirstVisibleCol = firstVisibleCol;
                            e.NewValue = firstVisibleCol;
                            chkLast = true;
                        }
                    }
                }
                else
                {
                    if (e.NewValue <= grid.GridHeaderList.Count - lastHScrollValue)
                    {
                        firstVisibleCol = e.NewValue;
                        grid.FirstVisibleCol = firstVisibleCol;
                        chkLast = false;
                    }
                }
            }
            else
            {
                if (e.NewValue < (grid.GridHeaderList.Count - lastHScrollValue))
                {
                    if (firstVisibleCol < grid.LastVisibleCol)
                    {
                        firstVisibleCol = e.NewValue;
                        grid.FirstVisibleCol = firstVisibleCol;
                        chkLast = false;
                    }
                }
            }
            CalcVisibleRange();
            ReCalcScrollBars();
            Invalidate();
        }

 

가로/세로 스크롤바를 재계산 하는 메소드. 세로 스크롤바 재계산 로직은 추후에 추가될 것이다.

        private void ReCalcScrollBars()
        {
            if (hScrollBar == null || vScrollBar == null) return;

            if (grid != null && grid.GridHeaderList != null) 

                allColsWidth = grid.GridHeaderList[grid.GridHeaderList.Count() - 1].Left + grid.GridHeaderList[grid.GridHeaderList.Count() - 1].Width;

 

            //컬럼의 폭이 클라이언트 사이즈 폭 보다 클 경우 가로 스크롤바를 보여준다.
            if ((allColsWidth > 0) && (allColsWidth > ClientSize.Width - ysclWidth))
            {
                hScrollBar.Visible = true;
                hScrollBar.LargeChange = ((lastVisibleCol - firstVisibleCol) + 1) / 2 + 1;
                lastHScrollValue = ((lastVisibleCol - firstVisibleCol) + 1) / 2 + 1;
            }
            else
            {
                hScrollBar.Visible = false;
                grid.FirstVisibleCol = 0; //Control 크기가 바뀌면서 hScrollBar가 가려지면 Grid의 첫번째 컬럼 부터 그려지도록 처리
            }
        }

 

WANIGrid Control Load 시에 스크롤바의 이벤트 핸들러를 초기화 한다.

        private void WANIGrid_Load(object sender, EventArgs e)
        {
            //더블버퍼링(Double Buffering) 처리
            this.SetStyle(ControlStyles.DoubleBuffer, true);
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.UserPaint, true);

            rc = new Rectangle(0, 0, this.ClientRectangle.Width, this.ClientRectangle.Height);
            EventHandlerInitialize(); //스크롤 이벤트 초기화
            InitializeScollBar();
            Invalidate();
        }

 

WANIGrid Control Resize 이벤트 시에 ReCalcScrollBars 추가 한다.

        private void WANIGrid_Resize(object sender, EventArgs e)
        {
            rc = new Rectangle(0, 0, this.ClientRectangle.Width, this.ClientRectangle.Height);
            InitializeScollBar();
            ReCalcScrollBars();
            Invalidate();
        }

 

WANIGrid Control Paint 이벤트 시에 ReCalcScrollBars 추가 한다.
        private void WANIGrid_Paint(object sender, PaintEventArgs e)
        {
            CalcVisibleRange();
            ReCalcScrollBars();
            DrawBackground(e.Graphics, rc);
            DrawHeaders(e.Graphics, rc);
        }

 

WANIGrid Control SizeChanged 이벤트 시에 ReCalcScrollBars 추가 한다.
        private void WANIGrid_SizeChanged(object sender, EventArgs e)
        {
            InitializeScollBar();
            ReCalcScrollBars();
            Invalidate();
        }

 

WANIGrid Control ClientSizeChanged 이벤트 시에 ReCalcScrollBars 추가 한다.
        private void WANIGrid_ClientSizeChanged(object sender, EventArgs e)
        {
            rc = new Rectangle(0, 0, this.ClientRectangle.Width, this.ClientRectangle.Height);
            InitializeScollBar();
            ReCalcScrollBars();
            Invalidate();
        }

 

이렇게 소스를 변경하고 실행하면 스크롤바를 움직일때 마다 컬럼이 바뀌어짐을 알 수 있다.

[그림 7-1] 실행 첫 화면
[그림 7-2] 스크롤 이벤트 발생 시 변경된 화면

이렇게 해서 WANIGrid Control의 가로 스크롤 이벤트 발생 시 Grid Header의 컬럼 정보를 볼 수 있도록 했다.

행이 추가 되기 전 전체 컬럼을 모두 살펴볼 수 있는 기능을 개발한 것이며, 오늘 까지 이렇게 만든 기능들은 추후 계속해서 변화 또는 개선이 되어질 것이다. 

그때 마다 변경된 사항들에 대해서 설명을 해 나갈 것이다.

지금까지의 소스는 아래의 첨부를 참조하기 바란다.

 

WANI Grid_20190620.zip
0.39MB

다음에는 행을 추가하기 위해 고려해야 할 사항들에 대해서 알아 보기로 하자.

 

 

반응형
반응형

Grid 전체 모습과 기능을 갖추기 위해서 Grid의 전체 기능을 총괄할 수 있는 Grid Class를 만들어 보기로 하는데 지금까지 Grid Header를 그리기 위한 작업들을 주욱 해 왔다. 따라서 Header 영역의 컬럼을 그릴 수 있는 기능을 구현해 보기로 하자.

 

Grid에 대한 모든 책임과 권한을 가지는 Class인 Grid Class를 생성해보기로 하겠다.

Grid의 전체 기능 중 Header 영역을 설정하고 그리는 기능을 추가하기 위해서 필요한 변수 부터 먼저 설정하기로 하자.

헤더를 생성하기 위한 HeaderGenerator 를 선언하고 Header의 높이와 Header 왼쪽의 빈 공간을 설정한다.

가로 스크롤바를 이용할 경우 현재 보여져야 할 화면 영역의 첫번째 Column과 마지막 Column 정보를 저장하기 위한 변수 선언, Header Title의 폰트 선언 및 설정을 한다.

private HeaderGenerator headerGen;
private int topHeaderHeight = 20;   //Grid의 Header 높이
private int leftHeaderWidth = 22; //Grid의 맨 왼쪽 빈Column Width
private int firstVisibleCol = 0;    //화면상에서 처음 보여져야할 컬럼
private int lastVisibleCol = 0;     //화면상에서 마지막에보여질 컬럼    
private Font headerFont = new Font("맑은 고딕", 9, FontStyle.Bold);
private SolidBrush blackBrush = new SolidBrush(Color.Black);

 

각 변수들을 Class 외부에서 get/set 할 수 있도록 Property 설정을 추가한다.

 

Header를 그리기 위한 DrawHeader 메소드를 만든다.

public void DrawHeader(Graphics graphics, Rectangle rect, int clientWidth)
{
    if (headerGen != null)
    {
        headerGen.DrawHeaders(firstVisibleCol, lastVisibleCol, clientWidth, graphics, rect);
    }
}

 

Header 영역을 그리기 위한 Grid Class의 정의는 이것으로 마무리하고, WANIGrid Control에 대해서 알아보자.

WANIGrid Control은 디자인 모드에서 개발자가 직접 가져다 사용할 수 있는 Control 이며, 실제 Grid의 제어 영역이기도 하다.

WANIGrid Control이 가장 Core이며 핵심이 되는 부분이다. 앞으로도 이 Control에 많은 기능들이 추가 될 것이다.

현재 WANIControl에서 Header영역을 그리기 위해 필요한 주요 기능들을 나열해 보자.

  • GridType에 따른 Grid 정보에 대한 Get/Set Method - 나타내고자 하는 Grid Type 설정
  • Grid의 왼쪽 빈 공간에 대한 폭 설정 - Grid 첫번째 컬럼의 폭을 기본으로 22로 설정을 했으나, 언제든 목적에 따라 폭을 변경할 수 있도록 하기 위해서 임.
  • 가로/세로 스크롤바 위치 선정을 위한 초기화 - Control이 생성 또는 변경 되는 사이즈에 따라 가로/세로 스크롤바의 위치를 초기화
  • Grid Header를 생성하기 위한 SetHeader Method - WANIControl 외부에서 Header 정보를 만들어서 설정할 수 있도록 지원
  • 현재 보여지는 Grid 영역의 계산을 담당하는 Method - 현재 보여지는 첫 컬럼/로우, 마지막 컬럼/로우를 계산
  • WANIGrid Control의 Backgroud를 그리는 DrawBackground Method - WANIGrid Control의 기본 Background 화면을 생성
  • Grid Header를 그리기 위한 DrawHeaders Method - GridType에 맞는 Grid 모습을 그린다
  • Control Event 처리 - Load/Resize/Paint/SizeChanged/ClientSizeChanged - WANIGrid Control의 변경 Event에 따른 Grid 모습을 다시 그릴 수 있도록 처리하기 위해서 이벤트 처리가 필요

생각보다 많은 기능들을 처리해야 하는 부분이라 현재는 소스가 이렇게 구성되지만 향 후에 Refactoring을 통한 많은 개선과 변화가 있을 수 있는 부분이기도 하다.

WANIGrid Control의 소스에 대한 대략적인 내용들을 살펴 보았다. 상기의 내용을 기반으로 소스를 읽어보면 한결 이해하기가 쉬울 것이다.

지금은 단순히 지정된 Grid Header와 제목만 보여주는 기능만을 가지고 있지만 앞으로 계속 진행하면서 주요 기능들을 하나씩 추가해 나갈 것이다.

좋은 아이디어나 개선사항이 있으면 언제든 주저하지 말고 댓글을 남겨주시길 바라며....

[그림 6-1] WANIGrid Control 실행 모습

간략하게 설명한 내용의 대한 소스코드는 아래의 파일을 참조하자.

WANI Grid_20190616.zip
0.28MB

 

 

 

반응형
반응형

[그림 5-1] 추상 팩토리 패턴을 적용한 Header 생성 관련 Class Diagram

추상 팩토리 패턴의 클래스 생성을 책임지는 HeaderBuilder Class를 살펴보기로 하자.

HeaderBuilder Class는 GridType에 따른 HeaderGenerator를 상속 받은 클래스를 이용해서 Header를 그리기 위한 클래스를 생성하는 책임을 가지게 된다.

먼저 GridType에 대해서 살펴보자.

GridType은 차후에 다양한 형태의 Grid를 그릴 수 있도록 아래와 같이 3가지 타입을 정의했다. 현재는 DefaultType을 기반으로 먼저 기본 기능들을 구현할 것이다. DefaultType의 기능이 구현되고 나면 추후에 YearMonthWeekNoType과 YearMonthWeekNoDayType 등을 구현하도록 하자.

    public enum GridType
    {
        DefaultType,    //Default Type
        YearMonthWeekNoType,    //년, 월, 주차 유형
        YearMonthWeekNoDayType  //년월, 주차, 일 유형
    }

 

HeaderBuilder Class는 GridType을 설정하고 가져오기 위해서 GridType 변수와 GridType에 따른 HeaderGenerator를 설정하기 위한 HeaderGenerator 변수를 가지게 된다.

    private GridType gridType = GridType.DefaultType;   //GridType 변수 선언 및 DefaultType 설정
    private HeaderGenerator headGen;    //HeaderGenerator 변수 선언

 

설정한 GridType 값을 HeaderBuilder Class 외부에서 get/set 하기 위한 Property

    public GridType GridDisplayType
    {
        get { return gridType; }
        set { gridType = value; }
    }

 

HeaderBuilder Class의 생성자는 파라미터가 없는 Default 생성자와 GridType을 파라미터를 입력 받는 생성자를 제공

    public HeaderBuilder() { }
    public HeaderBuilder(GridType grdType)
    {
        this.gridType = grdType;
        if (gridType == GridType.DefaultType) //현재는 GridType이 DefaultType일 경우만 설정함.
        {
            if (headGen == null) 

                headGen = new DefaultHeaderGenerator(); //GridType이 DefaultType일 경우 DefaultHeaderGenerator를 설정
        }
    }

 

Grid Header의 Header정보를 추가하는 메소드

    public void AddHeader(Header header)
    {
        if (gridType == GridType.DefaultType) //현재는 GridType이 DefaultType일 경우만 설정함.
        {
            if (headGen == null) headGen = new DefaultHeaderGenerator();
            headGen.AddHeaders(header);
        }
    }

 

추가된 Header 목록을 가져오는 메소드

    public<List> GetHeaders()
    {
        return headGen.GetHeaders();
    }

 

HeaderGenerator를 반환하는 Property

    public HeaderGenerator HeaderGen
    {
        get { return headGen; }
    }

 

Header 추가 후 Header를 초기화하는 메소드에서 Header를 그리기 위해서 필요한 각 Header의 순서인 Index값과 왼쪽 시작 X 좌표값을 계산하게 된다. 

    public void InitializeHeader()
    {
        //헤더 설정이 되어져 있지 않을 경우
        if (headGen == null || headGen.GetHeaders().Count < 1) return;

        int index = 0;
        int left = 0;

       

        // 등록된 Header 정보에서 Header Index값과 Left 좌표 값을 계산한다.
        foreach (Header header in headGen.GetHeaders())
        {
            header.Index = index;
            header.Left = left;
            left += header.Width;
            index++;
        }
    }

 

Grid Header를 그리기 위해서 필요한 클래스들을 모두 만들어 보았다. 아래의 소스 코드를 실행해도 Grid Header가 그려지거나 하지는 않지만 Grid Header를 그리기 위한 준비는 되었다.

다음 시간에는 Grid Class를 만들어서 실제 Grid Header를 그리는 메소드를 구현해 보겠다.

MyProject02.zip
0.26MB

 

 

 

반응형
반응형

[그림 4-1] 추상 팩토리 패턴을 적용한 Header 생성 관련 Class Diagram

지난 번 강좌에서 보았던 추상 팩토리 패턴의 Header 생성 클래스에 대해서 살펴 보기로 하자.

 

공통 Header 정보를 관리하는 추상클래스 Header 클래스를 살펴보자.

추상 클래스인 Header에는 반드시 가져야 하는 공통 속성 정보들을 관리하도록 구현했다.

각 속성 정보들은 외부에서 getter, setter 할 수 있도록 Property 형태로 제공한다.

 

public abstract class Header
{
    #region 변수
    private int index;  //Index
    private string columnId;    //컬럼 ID
    private string title;   //컬럼 타이틀
    private int left;   //컬럼 시작위치
    private int width;  //컬럼 폭
    private HorizontalAlignment headAlign;  //컬럼 타이틀 정렬위치
    private HorizontalAlignment textAlign;  //컬럼 내용 정렬위치
    private bool visible = true;    //컬럼 Visible 여부
    #endregion 변수

    #region Property
    public int Index
    {
        get { return index; }
        set { index = value; }
    }
    public string ColumnId {
        get { return columnId; }
        set { columnId = value; }
    }
    public string Title
    {
        get { return title; }
        set { title = value; }
    }
    public int Left
    {
        get { return left; }
        set { left = value; }
    }
    public int Width
    {
        get { return width; }
        set { width = value; }
    }
    public HorizontalAlignment HeadAlign
    {
        get { return headAlign; }
        set { headAlign = value; }
    }
    public HorizontalAlignment TextAlign {
        get { return textAlign; }
        set { textAlign = value; }
    }
    public bool Visible
    {
        get { return visible; }
        set { visible = value; }
    }
    #endregion Property
}

 

Header Type에 따라 Grid Header의 모습이 다양하게 그려질 수 있도록 공통 속성 외에 추가 속성과 생성자를 정의할 수 있도록 Header 클래스를 상속 받아서 구현한 클래스를 만들 수 있도록 했다.

향 후 Grid Control의 모습이 어느 정도 완성이 되면 다른 형태의 Grid Header를 만들어야 할 경우 Header 클래스를 상속받아서 구현한 클래스를 만들게 될 것이다.

현재는 기본 Header를 그릴 수 있는 DefaultHeader라는 클래스를 만들었다. 

DefaultHeader 클래스는 추상 클래스를 상속받아서 생성자를 6개 형태로 만들었다. 생성자 호출 시에 속성 정보를 파라미터로 받아서 처리할 수 있도록 하기 위해서 이다.

 

    public class DefaultHeader : Header
    {        
        public DefaultHeader()
        {
            this.ColumnId = "";
            this.Title = "";
            this.Width = 90;
            this.HeadAlign = HorizontalAlignment.Center;
            this.TextAlign = HorizontalAlignment.Left;
            this.Visible = true;
        }

        public DefaultHeader(string fieldName, string title, int width, HorizontalAlignment headAlign, HorizontalAlignment txtAlign, bool visible)
        {
            this.ColumnId = fieldName;
            this.Title = title;
            this.Width = width;
            this.HeadAlign = headAlign;
            this.TextAlign = txtAlign;
            this.Visible = visible;
        }
        public DefaultHeader(string fieldName, string title, int width, HorizontalAlignment txtAlign, bool visible)
        {
            this.ColumnId = fieldName;
            this.Title = title;
            this.Width = width;
            this.HeadAlign = HorizontalAlignment.Center;
            this.TextAlign = txtAlign;
            this.Visible = visible;
        }

        public DefaultHeader(string fieldName, string title, HorizontalAlignment txtAlign, bool visible)
        {
            this.ColumnId = fieldName;
            this.Title = title;
            this.Width = 100;
            this.HeadAlign = HorizontalAlignment.Center;
            this.TextAlign = txtAlign;
            this.Visible = visible;
        }

        public DefaultHeader(string fieldName, string title, int width, bool visible)
        {
            this.ColumnId = fieldName;
            this.Title = title;
            this.Width = width;
            this.HeadAlign = HorizontalAlignment.Center;
            this.TextAlign = HorizontalAlignment.Center;
            this.Visible = visible;
        }

        public DefaultHeader(string fieldName, string title, int width, HorizontalAlignment txtAlign)
        {
            this.ColumnId = fieldName;
            this.Title = title;
            this.Width = width;
            this.HeadAlign = HorizontalAlignment.Center;
            this.TextAlign = txtAlign;
            this.Visible = true;
        }        
    }

 

Grid Header를 생성하기 위해서 Header 정보를 추가(Add)하고 그리기 위한 메소드를 지정하는 추상 클래스를 만든다.

    public abstract class HeaderGenerator
    {
        protected List _headers = new List(); 

        protected int topHeaderHeight = 20;   //Grid의 Header 높이
        protected int leftHeaderWidth = 22; //Grid의 맨 왼쪽의 첫번째 빈Column Width
        protected Font headerFont = new Font("맑은 고딕", 9);
        protected SolidBrush blackBrush = new SolidBrush(Color.Black);

        public int TopHeaderHeight
        {
            get { return topHeaderHeight; }
            set { topHeaderHeight = value; }
        }

        public int LeftHeaderWidth
        {
            get { return leftHeaderWidth; }
            set { leftHeaderWidth = value; }
        }

        public Font HeaderFont
        {
            get { return headerFont; }
            set { headerFont = value; }
        }

        public SolidBrush BlackBrush
        {
            get { return blackBrush; }
            set { blackBrush = value; }
        }

        public HeaderGenerator() { }

        public abstract void AddHeaders(object obj);
        public abstract List GetHeaders(); 

        public abstract void HeaderClear();                         

        public abstract void DrawHeaders(int firstVisibleCol, int lastVisibleCol, int controlWidth, Graphics graphics, Rectangle rect);

    }

 

이제 HeaderGenerator라는 추상 클래스를 상속받아서 Header Type별로 Header를 그리는 클래스를 만들어야 한다.

현재는 기본 Header인 DefaultHeader를 설정하고 그리기 위한 DefaultHeaderGenerator 클래스를 만들어야 한다.

DefaultHeaderGenerator 클래스는 Grid의 Header 영역을 직접 그리는 영역이다.

특히 주의 깊게 봐야 할 부분은 DrawHeaders 메소드 부분이다. 

Grid의 Row와 Column이 아무리 많이 있다고 하더라도 Grid Control 사이즈에 맞는 영역의 Row와 Column만 사용자에게 제공하면 된다.

따라서 스크롤바를 움직이거나 움직이지 않았을 때 Grid Control에서 보여줘야 할 Row와 Column을 계산해서 찾아내야 한다. Header의 경우 항상 Control의 최상단 영역에 그려지는 부분이기에 Grid Control 영역에 맞는 첫번재 컬럼과 마지막 컬럼을 찾아서 헤더 정보를 그리면 된다.

그렇기에 DrawHeaders메소드의 파라미터로 firstVisibleCol, lastVisibleCol를 받고 있는 것이다.

 

    public class DefaultHeaderGenerator : HeaderGenerator
    {        
        /// 컬럼 헤더 정보를 설정
        public override void AddHeaders(object obj)
        {
            this._headers.Add(obj as Header);            
        }

        /// 컬럼 헤더 정보를 반환
        public override List GetHeaders()
        {
            return this._headers;
        }

        /// 헤더 정보를 Clear한다.
        public override void HeaderClear()
        {
            this._headers.Clear();
        }

        /// Header 그리기                 
        public override void DrawHeaders(int firstVisibleCol, int lastVisibleCol, int controlWidth, Graphics graphics, Rectangle rect)
        {
            SolidBrush brush = new SolidBrush(SystemColors.ControlLight);
            Pen pen = new Pen(Color.LightGray);

            int columnStartX = 0;
            graphics.FillRectangle(brush, columnStartX + 1, 1, leftHeaderWidth, topHeaderHeight);
            graphics.DrawRectangle(pen, columnStartX + 1, 1, leftHeaderWidth, topHeaderHeight);
            
            for (int i = firstVisibleCol; i <= lastVisibleCol; i++)
            {
                columnStartX = this._headers[i].Left + leftHeaderWidth;
                int headerWidth = this._headers[i].Width;

                //보여지는 컬럼의 폭이 컨트롤의 폭 보다 클경우 
                if (this._headers[i].Left + headerWidth > controlWidth)
                {
                    headerWidth = controlWidth - this._headers[i].Left - leftHeaderWidth - 3;
                }
                //헤더 영역의 사각형을 채우고 테두리를 그린다.
                graphics.FillRectangle(brush, columnStartX + 1, 1, headerWidth, topHeaderHeight);
                graphics.DrawRectangle(pen, columnStartX + 1, 1, headerWidth, topHeaderHeight);
                //헤더 타이틀 정렬 방법 설정
                StringFormat sf = new StringFormat();
                sf.LineAlignment = StringAlignment.Center;
                sf.Alignment = StringUtil.GetStringAlignment(sf, HorizontalAlignment.Center);
                //헤더 타이틀을 그린다.
                Rectangle colRec = new Rectangle(columnStartX + 1, 1, headerWidth, topHeaderHeight);
                graphics.DrawString(this._headers[i].Title, headerFont, blackBrush, colRec, sf);
            }
        }
    }
}

WANI Grid.zip
0.25MB

 

Grid Header를 그리기 위한 기본 클래스들에 대해서 살펴 보았다.

지금까지의 설명을 담은 소스는 위의 첨부 파일을 참조하면 된다.

다음 강좌에서는 Grid Header 영역을 그리기 위한 부분들을 추가해서 헤더가 그려지는 부분을 완성하도록 해보겠다.

이 강좌는 Step By Step 형태로 조그마한 기능 하나 하나를 추가/보완해 가면서 전체 기능을 완성하는 과정을 보여주고 나 또한 단계별 학습을 통해서 기능을 하나씩 완성해 가고자 한다.

 

단계별 진행을 보면서 추가 또는 보완했으면 하는 아이디어가 있다면 언제든 댓글을 달아주시길....

반응형
반응형

Grid Header 부분은 Grid의 Column 각각에 대한 타이틀과 속성 정보들을 관리하는 부분이라고 볼 수 있다.
우리가 흔히 보는 Grid Control의 모습은 아래와 같다.

[그림 3-1] Grid 영역

맨 첫번째 영역인 Default Column, Grid Header 영역은 각 컬럼의 데이터 속성을 대표하는 타이틀을 보여주는 Header영역과 Row와 Column별로 구분되어 실제 데이터를 입력 받거나 보여주기 위한 Data 영역으로 구성된다.
오늘 설명하고자 하는 부분은 각 Column의 타이틀 영역인 Header 부분에 대해서 관리되어야 할 정보와 내용들에 대해서 논하고자 한다.
Grid의 Header 영역은 Grid 최상단에 위치하며 초기 고정된 높이를 가지게 되며, 또한 Grid가 처음 만들어지고 초기화 될 때 Header 영역에 대한 속성이 설정되어진다. 
Grid 초기화 시에 고려되어야 하는 부분들 중 Header 영역으로만 제한해서 생각하면 먼저 첫번째 Column인 빈 영역과 데이터를 입력하거나 보여주게 되는 Column들로 구성이 된다.
각 Header에서 관리되어야 할 정보들은 컬럼 제목, 컬럼 폭, 컬럼 시작 위치, 컬럼 타이틀 정렬 방법(좌측/중앙/우측 정렬), 컬럼 내용 정렬 방법(좌측/중앙/우측 정렬), 컬럼 ID, 컬럼 Index, 컬럼 Display 여부 등의 정보들이다.

 

항목 필드명 상세
순서 Index (index) 컬럼 순서를 의미한다.
컬럼 ID ColumnId (columnId) 컬럼 ID를 말하며, DataTable과 연동될 경우 필드 ID를 의미한다.
타이틀 Title (title) 컬럼 타이틀
컬럼 시작 위치 Left (left) 컬럼의 시작 X좌표
컬럼 폭 Width (width) 컬럼의 폭
타이틀 정렬 방법 HeadAlign (headAlign) 컬럼 타이틀 정렬 방법 (좌측/중앙/우측)
컬럼 내용 정렬 방법 TextAlign (textAlign) Data 영역의 컬럼 내용 정렬 방법 (좌측/중앙/우측)
Display 여부 Visible (visible) 컬럼 Display 여부 (true/false)

Grid Header의 고유 속성은 상기의 8개 항목으로 정의한다. 분석/설계/구현을 진행하면서 속성이 변경될 소지는 다분히 있으나, 현재까지의 분석/설계 단계에서는 8개의 항목으로 결정하고 진행하겠다.

 

Grid Control을 사용하기 위해서는 반드시 Grid Header 부분에 대한 정의가 있어야 하며, 정의된 Header 정보를 근간으로 데이터를 보여주게 된다.

Grid Header는 Header 타입에 따라 다양한 모습으로 구성될 수 있다.

Grid Header를 초기화 할때 Header Type에 따라 다양한 모습으로 구성될 수 있도록 구현되어져야 하며, Header 속성 정보가 Data 영역의 컬럼들을 제어하게 된다. 

각 컬럼을 제어하게 되는 속성은 컬럼 순서, 컬럼 내용 정렬, 컬럼 Display 여부, 컬럼의 폭 등이 된다.

 

Grid Header 초기화 시에 동적으로 Header를 생성하기 위해서 디자인 패턴으로는 추상 팩토리 패턴을 사용했다.

추상 팩토리 패턴은 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴을 말하며 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성하는 경우에 유용하다.

 

[그림 3-2] 추상 팩토리 패턴을 적용한 Header 생성 관련 Class Diagram

Grid Header 생성을 위해서는 먼저 HeaderBuilder 클래스를 이용해서 Header 정보를 생성 후 HeaderBuilder를 WANIGrid Control에 SetHeader 메소드를 이용해서 설정을 하도록 설계 및 구현을 했다.

 

HeaderBuilder를 이용해서 Grid Header 생성은 아래와 같이 사용한다.

HeaderBuilder 생성자의 입력 파라미터로 GridDisplayType를 던지고 GridDisplayType에 따라 Grid Header의 형태가 결정되어진다.

builder.AddHeader메소드를 이용해서 DefaultHeader 클래스를 생성해서 Grid Header를 추가한다.

Header 추가가 완료 되면 InitializeHeader메소드를 이용해서 Header 속성 정보들을 초기화 한다. 초기화 완료 후 HeaderBuilder 클래스를 WANIGrid Control의 SetHeader메소드를 호출해서 설정하는 것으로 Grid Header 설정은 마무리 된다.

 

아래는 HeaderBuilder 생성 후 Grid Header 설정 예.

HeaderBuilder builder = new HeaderBuilder(this.waniGrid.GridDisplayType);
builder.AddHeader(new DefaultHeader("Col01", "Column 01", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col02", "Column 02", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col03", "Column 03", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col04", "Column 04", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col05", "Column 05", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col06", "Column 06", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col07", "Column 07", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col08", "Column 08", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col09", "Column 09", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.AddHeader(new DefaultHeader("Col10", "Column 10", 100, HorizontalAlignment.Center, HorizontalAlignment.Left, true));
builder.InitializeHeader();
this.waniGrid.SetHeader(builder);

 

다음 시간에는 위에 설명한 방법대로 구현한 Header 관련 클래스에 대한 설명과 소스를 같이 보도록 하자.

반응형
반응형

앞에 만들었던 WANIGrid 솔루션에 Grid Control에 걸맞는 레이아웃을 잡도록한다.

Grid Control에 VScrollBar와 HScrollBar를 추가하고 Control 사이즈 변경에 따라 VScrollBar와 HScrollBar가 제 위치를 찾아 갈 수 있도록 레이아웃을 만들도록 한다.

Control 사이즈를 넘는 많은 Row와 Column의 데이터를 제공하기 위해서 가로/세로 스크롤 바를 추가해서 전체 데이터를 볼 수 있도록 해야 한다.

 

WANIGrid Control의 사이즈는 기본적으로 Width: 200, Height: 150 으로 설정한다.

[그림 1] Control 사이즈 설정

HScrollBar와 VScrollBar를 추가한다.

HScrollBar의 Name은 hScrollBar, VScrollBar의 Name은 vScrollBar로 한다.

[그림 2] 가로/세로 스크롤바 추가

Control 초기화 되거나 사이즈가 변경될 때 ScrollBar를 초기화 할 수 있는 함수를 만들도록 한다.

ScrollBar초기화 시에는 가로 스크롤바의 위치와 세로 스크롤바의 위치를 Control 사이즈 내에서 맨 아래와 맨 우측에 위치할 있도록 설정을 해야 한다.

 

또한 세로 스크롤바의 경우 Grid Header 영역 아래에서 부터 시작을 해야 하기에 Grid Header 영역을 20으로 잡아서 Control 영역 상단의 20만큼의 공간을 비워두고 스크롤이 위치되도록 한다.

이때 Grid의 Header 영역을 보관하는 변수를 WANIGrid.cs에 추가하도록 한다.

 

private int topHeaderHeight = 20;   //Grid의 Header 높이

 

Control 내의 가로/세로 스크롤바의 위치를 초기화하는 메소드를 만든다.

        /// 
        /// HScrollBar, VScrollBar 초기화
        /// 
        public void InitializeScollBar()
        {
            //가로 스크롤바 설정
            hScrollBar.Left = 1;
            hScrollBar.Width = Width - vScrollBar.Width - 2; 
            hScrollBar.Top = Height - hScrollBar.Height - 2;

            //세로 스크롤바 설정
            vScrollBar.Left = Width - vScrollBar.Width - 2;
            vScrollBar.Top = topHeaderHeight + 2;
            vScrollBar.Height = Height - topHeaderHeight - hScrollBar.Height - 4;
        }

 

이렇게 만들어진 메소드 InitializeScollBar()를 Form Load 이벤트, Resize 이벤트, 사이즈 변경 이벤트, Client 사이즈 변경 이벤트가 발생할 때 호출하도록 한다.

 

        private void WANIGrid_Load(object sender, EventArgs e)
        {
            InitializeScollBar();
        }

        private void WANIGrid_Resize(object sender, EventArgs e)
        {
            InitializeScollBar();
        }

        private void WANIGrid_SizeChanged(object sender, EventArgs e)
        {
            InitializeScollBar();
        }

        private void WANIGrid_ClientSizeChanged(object sender, EventArgs e)
        {
            InitializeScollBar();
        }

 

이렇게 함으로써 Form의 사이즈가 변경될 때마다 가로/세로 스크롤바의 위치가 항상 Control의 Bottom과 Right에 위치하게 된다.

이렇게 Control의 기본 Layout에 대한 간단한 설정을 마무리 했다.

다음 회차에서는 이렇게 만들어진 Grid Control 레이아웃에서 그려져야 할 Grid Header 영역에 대해서 이야기 하고자 한다. Grid Header 영역은 분량이 많은 관계로 여러 번에 걸쳐서 설명을 할 예정이다.

 

지금까지 설명한 소스코드와 WANIGrid Control를 삽입해서 테스트 할 수 있는 WinForm 프로젝트가 아래의 zip파일에 포함되어져 있다.

압축을 풀고 WANI Grid 폴더 아래의 솔루션 파일을 읽어 들이면 WANIGridTest 프로젝트도 같이 포함된다.

WANI Grid.zip
0.21MB

반응형
반응형

Grid란 격자 모양을 의미하며, 격자 모양의 공간 즉 Cell 내부에 데이터를 보여주거나 입력하게 되는 컨트롤을 의미한다.

인터넷 상에는 Grid Control과 관련된 많은 소스와 방법들이 있다.

이미 많이 구현되어져 있고, 지금까지 내가 많이 사용해 왔던 Control이기에 앞으로 기본 기능부터 하나씩 구현하면서 점차 고유의 기능을 덧붙이고 기존의 방법과는 다른 방법을 제공하거나 제공되지 않았던 기능을 추가하면서 Grid Control을 발전시켜 나갈 계획이다.

가장 단순한 기능 부터 개발하면서 하나씩 기능을 추가해보는 경험을 함으로써 Windows Control 개발에 대한 경험과 고민, 생각들을 정리하고 이 글을 읽는 독자들과 함께 공유하고자 한다.

 

단계별로 하나씩 개발하면서 생각했던 사항과 구현된 소스를 같이 리뷰하며 보다 나은 방법을 고민하는 시간을 가질 수 있을 것으로 생각한다.

 

먼저  Grid Control에 대한 기본 구성은 

  • 헤더(Header) - Column에 대한 타이틀
  • Cell - 특정 Row와 Column으로 선택된 영역
  • 컬럼(Column) - 헤더(Header)에 종속되는 영역
  • 로우(Row) - 컬럼(Column)들로 구성된 하나의 행

으로 정의할 수 있다.

 

먼저 Grid Control의 Layout를 만들기 위해서 필요한 사항 부터 정리를 해보자.

  1. User Control을 생성 (WANIGrid)
  2. User Control에 데이터가 Grid Control 사이즈를 초과할 경우 필요한 VScrollBar와 HScrollBar 추가
  3. Control 사이즈에 맞추어 VScrollBar와 HScrollBar가 Bottom과 Right 영역에 위치하도록 설정

앞에서 이야기한 3개의 기능을 구현하기 위한 솔루션을 만들어 보기로 하자.

1. 프로젝트 또는 솔루션 열기를 선택

2. 새 프로젝트 만들기에서 Windows Forms 컨트롤 라이브러리(.Net Framework)를 선택하고 다음 버튼을 클릭한다.

3. 새 프로젝트 구성에서 프로젝트 이름과 폴더 위치를 선택해서 만들기 버튼을 클릭.

이렇게 해서 Grid Control을 만들기 위한 솔루션 및 프로젝트 구성은 완료되었다.

 

다음 과정에서는 만들어진 User Control 상에서 VScrollBar와 HScrollBar를 위치 시켜서 만들어보기로 하자.

여기까지 만든 소스 코드는 아래의 파일을 다운로드한다.

WANI Grid.zip
0.17MB

반응형
반응형

오랜 기간 Java와 Spring Framework으로 Web 기반의 어플리케이션을 주로 개발 및 운영을 해왔던 나로서는 C#으로 시작하는 Windows 기반의 프로그램 개발이 굉장히 생소했다.

개발 언어로만 본다면 문법과 예약어 등은 크게 어려울 것이 없었으나, Windows 기반의 경험이 신입사원 시절에 다루어본 Visual Basic 6 외에는 전무했던 상황에서 C# 언어로 Windows 기반의 WinForm 어플리케이션에서 사용하는 Control 개발 및 유지보수는 상당히 버거운 작업이었다.

1년 반 정도의 고된 시간을 보냈고 지금도 고된 시간을 버티며 하나씩 깨우쳐 나가는 나의 모습을 기록해보고자 한다.

데이터를 기반으로 시각화 된 컨트롤 객체와 그래픽 기반의 UI 생성 및 유지보수는 생소하고 낯선 영역이었기에 정말 너무나 힘든 부분이었던 것 같다.

지금은 조금 나아져서 어느 정도 이해와 왜 이렇게 해야만 하는지를 조금씩 알게 되었지만 이전에는 왜 이렇게 해야 하는지 어떻게 해야 하는지를 예측이나 가늠할 수 없었던 시기가 있었다.

 

나의 Windows 프로그램 개발 경험은 1997년 입사해서 Visual Basic 6 으로 개발을 시작했지만 2000년 접어 들면서 Java 기반의 Web 개발로 업무가 바뀌게 되었고 그 이후로 주욱 Java 기반의 Web 개발 및 운영을 해왔다.

개발 언어로서 Java를 25년 이상 사용해오다 2017년 하반기 부터 C#을 주요 개발 언어로 사용하기 시작했다.

이때 맡게된 시스템의 운영과 개발 업무를 시작하면서 부딪히게 되는 숱한 난관들에 얼마나 많은 좌절과 의기소침의 시간을 보냈는지 모른다.

 

C# 이라는 언어는 익혔지만 이걸로 뭘 할지...업무에 적용해야 하는 컨트롤들은 어떻게 만들어야 할지...상용/오픈소스 기반의 Grid Control 들은 어떻게 만들 수 있는지... 등등 

숱한 궁금증과 더불어 내가 생각하는 Grid Control을 만들어 보면 어떨까하는 생각에 시작을 하게 되었다.

먼저 간단한 기능 중심의 Grid Control을 만들면서 분석/설계에 대한 내용과 향 후 개선해야 할 점 등을 정리하고 점차 개선되어져 가는 Grid Control을 소개할 예정이다.

만들어진 Grid Control 소스는 공개를 할 것이며, 누구나 자유롭게 이용할 수 있도록 할 것이다.

 

이 블로그는 C# 개발자로 이제 막 시작하는 초보 개발자인 내가 C#과 Windows에 대해서 익히고 생각했던 내용들을 정리하면서 나 스스로가 향상되고 있음을 느끼고 조금씩 성장하는 나의 모습의 기록이기도 하다, 

이제 막 개발의 길로 들어선 초보개발자나 나와 같은 상황에서 C# 기반의 Windows 프로그램을 시작하는 이들에게 조금이나마 내 글이 도움이 될 수 있었으면 한다.

 

 

반응형

+ Recent posts