반응형

앞에서 WANIGrid Control 내의 마우스 우측 버튼 클릭 시 나타나게 되는 Context 메뉴 부분을 마무리 하도록 하자.

먼저 WANIGrid 내의 마우스 우측 버튼 클릭 시 Context Menu를 제공할지 말지를 설정할 수 있도록 바꾸면 좋을 듯 싶다. 마우스 우측 버튼 클릭 시 Context Menu가 나타나지 않도록 설정을 부여해야만 할 경우도 있을 수 있기 때문이다. 예를 들어 데이터를 WANIGrid Control에 조회만 할 수 있도록 하고, 새로운 행(Row)을 추가/삭제할 수 없도록 해야 할 경우도 있기 때문이다.

이러한 설정을 할 수 있도록 WANIGrid.cs파일의 변수 설정 부분에 아래의 코드를 추가하도록 한다.

ContextMenu를 보여줄지 말지를 설정할 수 있는 변수인 bool타입의 contextMenuShow

    private bool isShowContextMenu = true;    //ContextMenu 제공 여부

 

Property 설정 부분에 아래의 Code를 추가한다. 이렇게 WANIGrid Control 내에서 Mouse 우측 버튼 클릭 시 Context Menu 제공 여부를 설정할 수 있는 변수와 Property를 작성했다.

    ///<summary> 
    /// 마우스 우측 버튼 클릭 시 ContextMenu 제공 여부
    ///</summary> 
    public bool IsShowContextMenu
    {
        get { return isShowContextMenu; }
        set { isShowContextMenu = value; }
    }

 

Mouse Down 이벤트 메소드에서 Context Menu를 보여주는 로직을 처리하다 보니 소스 라인이 너무 길어지면서 가독성이 떨어지는 부분이 있어서 Context Menu를 보여주는 부분을 별도의 메소드로 분리를 하자. 

선택된 행(Row)의 수가 없거나 1개일 경우, 1개 이상일 경우에 따라 사용할 수 있는 Contex Menu의 활성화/비활성화 부분을 추가한다.

  • 선택된 행(Row)의 갯수가 0보다 작거나 같은 경우에는 "행 추가" 메뉴만 활성화
  • 선택된 행(Row)의 갯수가 1개일 경우에는 모든 메뉴를 활성화
  • 선택된 행(Row)의 갯수가 2개 이상일 경우 "행 추가", "행 삭제" 메뉴만 활성화 

    /// <summary> 
    /// Context Menu를 보여준다.
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary> 
    private void ShowContextMenu(object sender, MouseEventArgs e)
    {
        if (!isShowContextMenu) return;
        //마우스 우측 버튼이 클릭된 좌표 값을 얻어온다.
        Point p = new Point(e.X, e.Y);
            
        if (selectedRows.Count <= 0)
        {
            rightClickMenu.MenuItems[0].Enabled = false;
            rightClickMenu.MenuItems[1].Enabled = false;
            rightClickMenu.MenuItems[2].Enabled = true;
            rightClickMenu.MenuItems[3].Enabled = false;
        }
        else if (selectedRows.Count == 1)
        {
            rightClickMenu.MenuItems[0].Enabled = true;
            rightClickMenu.MenuItems[1].Enabled = true;
            rightClickMenu.MenuItems[2].Enabled = true;
            rightClickMenu.MenuItems[3].Enabled = true;
        }
        else if (selectedRows.Count > 1)
        {
            rightClickMenu.MenuItems[0].Enabled = false;
            rightClickMenu.MenuItems[1].Enabled = false;
            rightClickMenu.MenuItems[2].Enabled = true;
            rightClickMenu.MenuItems[3].Enabled = true;
        }

        rightClickMenu.Show(this, p);
    }

 

[그림 17-1] 행(Row)이 없을 때의 Context Menu
[그림 17-2] 1개의 행(Row)를 선택했을 때 Context Menu
[그림 17-3] 2개 이상의 행(Row)를 선택했을 때 Context Menu

 

Mouse Down 이벤트 발생 시 호출되는 WANIGrid_MouseDown 이벤트를 아래와 같이 변경한다.

    /// <summary> 
    /// Mouse Down Event
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary> 
    private void WANIGrid_MouseDown(object sender, MouseEventArgs e)
    {
        //마우스 우측 버튼 클릭 시 Context 메뉴 제공
        if (e.Button == MouseButtons.Right)
        {
            //WANIGrid Header 영역이 선택되어졌을 경우에는 메뉴 제공하지 않음.
            if (e.Y < grid.TopHeaderHeight) return;

            ShowContextMenu(sender, e);                
        }
        else if (e.Button == MouseButtons.Left)
        {

          :

          :

        }

    }

 

앞서 작성했던 Context Menu에 실제 클릭 이벤트 부분을 추가해 보도록 하자.

기존에 Context 메뉴는 만들었으나, 실제 동작하는 메뉴는 행 추가 메뉴 뿐이었다.

나머지 3개 메뉴에 대한 동작들도 같이 완성시켜 보도록 하겠다.

이전 행에 추가(OnMenu_BeforeInsertRow_Click), 다음 행에 추가(OnMenu_AfterInsertRow_Click), 행 삭제(OnMenu_DeleteRow_Click) 메뉴에 대한 이벤트 등록을 추가했다.

    /// <summary> 
    /// 마우스 우측 버튼 클릭시 제공 되는 메뉴 초기화
    /// </summary> 
    private void InitializeContextMenu()
    {
        //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);

        rightClickMenu.MenuItems[0].Click += new EventHandler(OnMenu_BeforeInsertRow_Click);
        rightClickMenu.MenuItems[1].Click += new EventHandler(OnMenu_AfterInsertRow_Click);
        rightClickMenu.MenuItems[2].Click += new EventHandler(OnMenu_AppenRow_Click);
        rightClickMenu.MenuItems[3].Click += new EventHandler(OnMenu_DeleteRow_Click);
    }

 

먼저 행 추가 메뉴를 클릭했을 때 호출되는 AppendRow() 메소드를 아래와 같이 보완한다.

    /// <summary> 
    /// Row 추가
    /// </summary> 
    public void AppendRow()
    {
        DataRow row = dataSource.NewRow();
        Row r = new Row(row);
        rows.Add(r);
        dataSource.Rows.Add(row);            
        rowHeight = Font.Height + 4;
        allRowsHeight += rowHeight;            
        OnRowsChanged();
    }

 

Context Menu 중 행 삭제를 선택했을 때 호출되는 이벤트를 살펴보자.

선택한 행의 갯수 만큼 Looping 돌면서 DeleteRow(int delRow) 메소드 호출과 함께 삭제된 행의 Index정보를 재설정하는 RebuildSelectedRowsIndex 메소드를 호출한다.

선택된 행을 모두 삭제하고 나면 selectedRows 리스트를 Clear한다.

    /// <summary>
    /// 선택한 행(Row) 삭제
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary>
    protected void OnMenu_DeleteRow_Click(object sender, EventArgs e)
    {
        if (selectedRows.Count > 0)
        {
            for (int i = 0; i < selectedRows.Count; i++)
            {
                int crntRow = selectedRows[i];
                DeleteRow(crntRow);
                RebuildSelectedRowsIndex(crntRow);
            }
            selectedRows.Clear();
        }
    }

 

행 삭제 메뉴 클릭 시 선택된 행(Row) 번호를 입력받아 동작하게 되는 DeleteRow(int delRow)는 아래와 같다.

    /// <summary>
    /// Row 삭제
    /// <param name="delRow"></param>
    /// </summary>
    public void DeleteRow(int delRow)
    {
        if (delRow < rows.Count && delRow >= 0)
        {
            DataRow row = rows[delRow].DataRow; //전체 Row중에 선택 Row를 가져온다.
            row.Delete(); //해당 Row 정보를 delete한다.                
            rows.RemoveAt(delRow);  //전체 Row중에 선택된 Row를 제거한다.               
            allRowsHeight -= rowHeight; //전제 Row의 높이에서 선택된 Row의 높이를 뺀다
            OnRowsChanged();
        }
    }

 

행 삭제 후 삭제해야 할 행 목록에서 행의 Index 정보를 재설정하는 메소드인 RebuildSelectedRowsIndex를 살펴보자.

이 메소드가 처리하는 것은 선택된 행(Row)의 목록에서 현재 선택된 행 이후의 행(Row)의 Index값을 1씩 빼서 재설정하는 것이다.

    /// <summary>
    /// 1개의 Row 삭제 후 선택한 행들의 Index 정보를 갱신한다.
    /// <param name="index"></param>
    /// </summary>
    private void RebuildSelectedRowsIndex(int index)
    {
        for (int i = index; i < selectedRows.Count; i++)
        {
            int reIndex = selectedRows[i] - 1; 
            selectedRows[i] = reIndex;
        }
    }

 

이전 행에 추가 메뉴 선택 시 수행되는 이벤트인 OnMenu_BeforeInsertRow_Click 이벤트는 사용자가 마우스 좌측 버튼을 클릭해서 Activie된 셀(Cell)에서 행(Row) 정보를 가져와서 해당 행 이전의 위치에 행(Row)을 추가 한다.

이전 행의 추가는 현재 행(Row)에 행(Row)을 추가하면 이전 행 추가가 된다.

    /// <summary>
    /// 선택 Row 앞에 행(Row)을 추가
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary>
    protected void OnMenu_BeforeInsertRow_Click(object sender, EventArgs e)
    {
        int crntRow = ActiveCell.Row;
        BeforeInsert(crntRow);
    }

 

    /// <summary>
    /// 이전 행에 Row 추가
    /// <param name="crntRow"></param>
    /// </summary>
    public void BeforeInsert(int crntRow)
    {
        DataRow row = dataSource.NewRow(); //DataTable인 dataSource에 새로운 행의 DataRow 생성
        Row r = new Row(row); //새로운 Row를 생성
            
        if (crntRow < 0) crntRow = 0; //crntRow의 값이 0보다 작으면 0으로 설정
        rows.Insert(crntRow, r);   //전제 Row를 관리하는 List인 rows에 생성된 Row를 추가 
        dataSource.Rows.InsertAt(row, crntRow); //생성된 DataRow를 DataTable에 원하는 위치에 Insert
        allRowsHeight += Font.Height + 4;   //전체 Row의 높이 값을 저장하는 allRowsHeight에 추가되는 Row의 높이 값을 더한다.
        rowHeight = Font.Height + 4; //1개 Row의 높이 값을 rowHeight 변수에 저장
        OnRowsChanged();
    }

 

다음 행에 추가 메뉴 선택 시 수행되는 이벤트인 OnMenu_AfterInsertRow_Click 이벤트는 사용자가 마우스 좌측 버튼을 클릭해서 Active된 셀(Cell)에서 행(Row) 정보를 가져와서 해당 행 다음의 위치에 행(Row)을 추가한다.

    /// <summary>
    /// 선택 Row 뒤에 행(Row)을 추가
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary>
    protected void OnMenu_AfterInsertRow_Click(object sender, EventArgs e)
    {
        int crntRow = ActiveCell.Row;
        AfterInsert(crntRow);
    }

 

    /// <summary>
    /// 다음 행에 Row 추가
    /// <param name="crntRow"></param>
    /// </summary>
    public void AfterInsert(int crntRow)
    {            
        if (crntRow + 1 == rows.Count) //현재 행(Row)가 마지막 행(Row)일 경우
        {
            AppendRow();
        }
        else
        {
            DataRow row = dataSource.NewRow();  //DataTable인 dataSource에 새로운 행의 DataRow 생성
            Row r = new Row(row);    //새로운 Row를 생성
            rows.Insert(crntRow + 1, r);    //핸재 행의 다음행에 추가
            dataSource.Rows.InsertAt(row, crntRow + 1);     //생성된 DataRow를 DataTable의 현재행 다음에 Insert
            allRowsHeight += Font.Height + 4;   //전체 Row의 높이 값을 저장하는 allRowsHeight에 추가되는 Row의 높이 값을 더한다.
            rowHeight = Font.Height + 4;    //1개 Row의 높이 값을 rowHeight 변수에 저장
            OnRowsChanged();
        }
    }

 

이렇게 해서 이전에 생성했던 Context Menu의 모든 기능을 구현했다. 구현한 메뉴를 테스트 하다보면 오류나 보완해야 할 부분들이 발생하게 된다.

이러한 작업들을 추가한 부분들도 같이 살펴 보기로 하자.

 

선택한 셀(Cell)에서 값이 변경되었을 때 호출되는 이벤트 부분에 전체 행(Row) 목록 변수인 rows의 Count가 0일 경우 값을 보여주기 위한 로직을 타지 않도록 처리해야 한다.

    /// <summary>
    /// TextBox인 editBox의 TextChanged Event
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary>
    private void EditBox_TextChanged(object sender, EventArgs e)
    {
        if (rows.Count < 1)
        {
            editBox.Visible = false;
            return;
        }

        DataRow row = rows[ActiveCell.Row].DataRow;
        Header header = grid.GridHeaderList.Where(x => x.Index == ActiveCell.Col).FirstOrDefault();
        if (header != null)
        {
            if (ActiveCell_ActvieCol > -1) row[header.ColumnId] = editBox.Text;
        }
    }

 

마우스 좌측 버튼을 눌러서 특정 셀(Cell)을 선택했을 때 파란색 테두리로 보여주던 부분을 회색의 점선으로 테두리를 그리도록 변경했다. 생뚱맞은 파란색 테두리 보다는 회선 점선이 일반적이다는 의견을 반영한 부분이다.

    /// <summary>
    /// Active Cell을 표기한다.
    /// <param name="g"></param>
    /// </summary>
    private void DrawActiveCell(Graphics g)
    {
        if (ActiveCell.Row != -1 && ActiveCell.Col != -1)
        {
            //파선 패턴 설정
            float[] dashValues = { 1, 1, 1, 1 };
            Pen grayPen = new Pen(Color.Gray, 1);
            grayPen.DashPattern = dashValues;                
            g.DrawRectangle(grayPen, GetSelectedCellRect(ActiveCell.Row, ActiveCell.Col));                                
        }
    }

 

[그림 17-4] 선택된 셀(Cell)의 테두리 라인 변경

 

선택한 셀(Cell)의 영역을 반환하는 메소드인 GetSelectedCellRect 메소드에서 전체 행(Row) 목록을 관리하는 rows의 Count가 0일 경우에 EndEdit메소드를 호출하고 하위 로직을 타지 않도록 보완했다.

    /// <summary>
    /// 선택한 Cell의 영역을 반환
    /// <param name="row"></param>
    /// <param name="col"></param>
    /// <returns></returns>
    /// </summary>
    protected Rectangle GetSelectedCellRect(int row, int col)
    {
        if (row < firstVisibleRow || row > lastVisibleRow) return new Rectangle(0, 0, 0, 0);
        if (col < firstVisibleCol || col > lastVisibleCol) return new Rectangle(0, 0, 0, 0);

        //선택된 Cell의 높이를 구한다.
        int top = topHeaderHeight;
        int height = 0;
        for (int i = firstVisibleRow; i <= lastVisibleRow; i++)
        {
            if (rows.Count == 0)
            {
                EndEdit();
                continue;
            }
            height = rows[i].MaxLines * rowHeight;
            if (row == i) break;
            top += height;
        }

       :

       :

    }

 

기존 코드에서 여러 번 수행되어야 하는 영역을 새로운 메소드로 분리해서 호출해서 사용하도록 변경해보자.

마우스 좌측 버튼 클릭 시 행(Row) 선택 영역의 색을 변경하는 부분의 소스를 SelectedRowsChangeColor라는 변수로 분리해 내었다.

    private void SelectedRowsChangeColor(int row)
    {
        if (Control.ModifierKeys != Keys.Control && Control.ModifierKeys != Keys.Shift)
        {
            if (selectedRows.Contains(row))
            {
                if (selectedRows.Count > 1) //선택된 행(Row)이 2개 이상일 경우
                {
                    selectedRows.Clear();   //여러 행(Row)가 선택된 경우 기존의 선택된 행(Row) 무효화
                    selectedRows.Add(row);  //선택 행(Row) 추가
                }
                else selectedRows.Remove(row);  //동일한 행(Row)를 2번 선택하면 선택 표시 지움
            }
            else //선택된 행(Row)가 없을 경우 기존 선택 행(Row)를 모두 지우고 선택한 행(Row)를 추가
            {
                selectedRows.Clear();
                selectedRows.Add(row);
            }
        }
        else
        {
            if (Control.ModifierKeys == Keys.Shift && selectedRows.Count > 0)
            {
                int index = selectedRows[0];
                int begin = Math.Min(row, index);
                int end = Math.Max(row, index);
                selectedRows.Clear();
                for (int i = begin; i <= end; i++)
                {
                    selectedRows.Add(i);
                }
            }
            else if (Control.ModifierKeys == Keys.Control)
            {
                if (selectedRows.Contains(row)) selectedRows.Remove(row);   //선택된 행(Row)을 다시 선택할 경우 제거해서 행(Row) 선택 무효화
                else selectedRows.Add(row); //선택된 행(Row)를 추가
            }
        }
        //Row를 선택하면 EditBox를 감춘다.
        editBox.Visible = false;
    }

 

이렇게 분리된 메소드 SelectedRowsChangeColor를 호출하는 메소드인 MouseLeftButtonClickInContents 이다.

이 부분에 있는 선택된 영역의 색상 변경을 하기 위해 처리하는 영역 중 중복된 부분을 SelectedRowsChangeColor라는 메소드로 뽑아 낸 것이다.

이렇게 함으로써 이 메소드의 소스 길이도 상당히 짧아지는 효과를 볼 수 있게 되었다.

    /// <summary>
    /// WANIGrid Control의 맨 왼쪽 컬럼에서 마우스 좌측 버튼을 눌렀을 경우
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// </summary>
    private void MouseLeftButtonClickInContents(object sender, MouseEventArgs e)
    {
        selectedCols.Clear();            
        int row = GetRowFromY(e.Y);
        int col = GetColFromX(e.X);
        if (row < 0) return;    //row값이 -1이면 처리하지 않음
        if (e.X < leftHeaderWidth)  //맨 좌측의 첫 컬럼을 선택했을 시 Row를 선택하도록 처리
        {
            SelectedRowsChangeColor(row);

        }

        else //WANIGrid Control 내부의 Content 영역을 마우스 좌측 버튼으로 클릭했을 때

       {

            selectedRows.Clear();
            selectedCols.Clear();

            if (row == ActiveCell.Row && col == ActiveCell.Col)
            {
                Rectangle r = GetSelectedCellRect(ActiveCell.Row, ActiveCell.Col);
                int k = 0;                    
                for (int i = r.Top; i < r.Bottom && k < rows[row].MaxLines - 1; i *= rowHeight, k++)
                {
                    if (e.Y >= i && e.Y <= i + rowHeight) break;
                }
                if (ActiveCell_ActiveRow != k) EndEdit();
                ActiveCell_ActiveRow = k;
                BeginEdit();    
            }
            else
            {
                ActiveCell.Row = row;
                ActiveCell.Col = col;
                EndEdit();
                SelectedRowsChangeColor(ActiveCell.Row);
            }

       }

    }   

 

다음 강좌에서는 점점 크기가 커지고 있는 WANIGrid.cs 파일을 보다 효율적으로 관리하기 위한 방법에 대해서 공유하고자 한다.

기능이 추가되면서 점점 소스코드가 길어지는 문제가 발생하고 있는데...

이 부분에 대해서 보다 효과적으로 대처하기 위한 아이디어와 방법을 찾아보기로 하자.

 

지금까지의 소스는 아래를 참조하자.

WANI Grid_20190825.zip
0.39MB

 

반응형
반응형

WANIGrid Control의 겉 모습을 어느 정도 구현을 했다. 행(Row) 추가 기능을 통해서 새로운 행들을 생성하고 가로/세로 스크롤바로 WANIGrid Control 내의 모습을 볼 수 있었다. 또한 마우스 휠을 이용해서 가로/세로 스크롤바를 움직인 것과 동일한 효과도 주었다.

 

여기에 추가로 Grid의 Top Header 영역을 클릭하면 해당 컬럼의 전체 로우를 선택했음을 표현할 수 있도록 색상을 변경하는 기능을 추가해 보기로 하자.

이번 시간에는 Grid의 Top Header 영역에서 마우스 좌측 버튼을 누르면 해당 위치의 컬럼을 선택한 것으로 표시하는 기능 개발하고자 한다.

 

WANIGrid.cs 파일을 열어서 선택한 컬럼이 한 개일 수도 있지만 2개 이상의 컬럼을 선택할 수도 있다. Control Key를 누르고 마우스 좌측 버튼을 눌렀을 경우에는 1개 이상의 컬럼을 선택할 수 있도록 처리할 것이다.

마우스 좌측 버튼만 눌렀을 때에는 한 개의 컬럼만 선택될 수 있도록 처리하도록 한다.

WANIGrid Header 영역의 맨 왼쪽 컬럼을 선택했을 경우는 아무런 처리가 되지 않도록 한다. 향 후에 이 영역을 선택했을 경우 모든 영역을 선택하도록 할 수도 있겠지만 지금은 아무런 동작을 하지 않도록 한다.

 

선택된 컬럼의 Index정보를 관리하기 위한 변수, 선택된 영역의 색상을 지정할 수 있도록 지원하기 위한 변수와 현재 마우스의 좌측 버튼 클릭 시 X, Y 좌표를 저장하기 위한 변수를 선언하도록 한다.

 

private List<int> selectedCols = new List<int>();   //선택된 컬럼(Columns)들을 관리하기 위한 변수

private Point mousePoint = new Point(0, 0); //Click한 마우스 좌표를 저장하기 위한 변수
private SolidBrush selectedColor = new SolidBrush(Color.LightCyan);

 

선택된 영역의 색상은 변경하기 위한 Property를 아래와 같이 설정한다. 디폴트 색상이 아닌 다른 색상을 지정하고자 할 경우 아래의 Property를 이용해서 색상을 변경할 수 있다.

 

public Color SelectedColor
{            
    set { selectedColor = new SolidBrush(value); }
}

 

실제 마우스 좌측 버튼이 클릭 되었을 때 마우스 X좌표의 위치로 컬럼의 Index정보를 반환하는 메소드는 아래와 같다.

 

private int GetColFromX(int X)
{
    int col = 0;
    int tempWidth = leftHeaderWidth; //맨 첫번째 비어 있는 컬럼의 폭

    if (X >= 0 && X <= leftHeaderWidth) //마우스 좌측 버튼 클릭 시 X좌표가 leftHeaderWidth 영역 내에 있을 경우에는 col변수에 -1값을 설정
    {
        col = -1;
    }
    else
    {

        //마우스 좌측 버튼 클릭 시 X좌표의 위치가 현재 보여지는 컬럼 내의 위치에 해당하는지 체크
        for (col = firstVisibleCol; col < lastVisibleCol; col++)
        {
            int width = grid.GridHeaderList[col].Width;
            if (X < width + tempWidth) break; //X좌표 위치와 컬럼 Index가 매핑 되었을 경우
            tempWidth += width;
        }
    }
    if (col > grid.GridHeaderList.Count - 1) col = -1; //매핑된 컬럼 Index값이 실제 컬럼 개수보다 많을 경우 -1을 반환
    return col;

}

 

마우스 좌측 버튼 클릭 시 X좌표에 해당하는 컬럼 Index를 가져오는 메소드까지 만들었다.

이제는 마우스 버튼이 눌러졌을 때 발생하는 이벤트에 마우스 좌측 버튼 클릭 부분을 추가하기로 하자.

MouseDown 이벤트 처리 부분을 살펴보자.

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);
    }
    else if (e.Button == MouseButtons.Left)
    {
        //WANIGrid Top Header 영역을 마우스 좌측 버튼으로 클릭했을 때
        if (e.Y < topHeaderHeight)
        {
            MouseLeftButtonClickInTopHeadHeight(sender, e); 
        }         
    }
}

 

실제 WANIGrid Top Header 영역이 클릭 되었을 때 호출되는 MouseLeftButtonClickInTopHeaderHeight 메소드는 아래와 같다.

private void MouseLeftButtonClickInTopHeadHeight(object sender, MouseEventArgs e)
{
        selectedRows.Clear();

        //컬럼이 존재할 경우
        if (grid.GridHeaderList.Count > 0)
        {
            int col = GetColFromX(e.X);
            if (col < 0) return; //col값이 -1이면 처리하지 않음

            //Control Key를 누르지 않은 상태에서 컬럼을 선택했을 경우
            if (ModifierKeys != Keys.Control)
            {
                //선택된 컬럼일 경우
                if (selectedCols.Contains(col))
                {
                    if (selectedCols.Count > 1) //선택된 컬럼이 두 개 이상일 경우
                    {
                        selectedCols.Clear(); //여러 컬럼이 선택된 경우 기존의 선택된 컬럼 무효화
                        selectedCols.Add(col); //선택 컬럼 추가
                    }
                    else selectedCols.Remove(col);  //동일한 컬럼을 2번 선택하면 선택 표시 지움
                }
                else //선택된 컬럼이 없을 경우 기존 선택 컬럼을 모두 지우고 선택한 컬럼을 추가
                {
                    selectedCols.Clear();
                    selectedCols.Add(col);
                }
        }
        else
        {
                    if (selectedCols.Contains(col)) selectedCols.Remove(col);   //선택된 컬럼을 다시 선택할 경우 제거해서 컬럼 선택 무효화
                    else selectedCols.Add(col); //선택된 컬럼을 추가
        }
        Invalidate();
    }

    //마우스 좌측 버튼 클릭 시 X/Y좌표를 저장
    mousePoint.X = e.X;
    mousePoint.Y = e.Y;
}

 

지금까지는 마우스 좌측 버튼 클릭 시 마우스 X좌표 위치에 대응하는 컬럼의 Inxex 값을 찾기 위한 로직을 만들었고, 실제 사용자에게 선택된 컬럼을 색상으로 보여주는 부분을 추가해야 실제 WANIGrid Control 실행 시에 사용자와 직접 상호작용하면서 선택된 컬럼 영역을 다이나믹하게 보여주게 된다.

 

기존의 DrawBackground 메소드에 선택된 컬럼의 배경 색상을 변경하는 기능을 아래와 같이 추가해야 한다.

 

    //선택된 컬럼의 Background를 그린다.
    for (int i = 0; i < selectedCols.Count; i++)   //선택된 컬럼의 갯수 만큼 반복
    {
        int index = selectedCols[i];
        int left = leftHeaderWidth;

        //WANIGrid Control 영역에서 보여지고 있는 컬럼 내에서 선택된 컬럼의 폭을 구한다.
        for (int j = firstVisibleCol; j < lastVisibleCol && j < index; j++)
        {
            left += grid.GridHeaderList[j].Width;
        }

        if (allRowsHeight > 0) //추가된 행(Row)가 있을 경우에만 처리
        {             
            g.FillRectangle(selectedColor, left + 1, topHeaderHeight, grid.GridHeaderList[index].Width + 1, 

                                 allRowsHeight - (rowHeight * firstVisibleRow) + 1);
        }
    }

 

이렇게 구현한 WANIGrid Control이 실행되었을 때 화면이다.

[그림 11-1] 1개의 컬럼을 선택했을 경우
[그림 11-2] 컨트롤 키를 누른 상태에서 여러 컬럼을 선택했을 경우

WANIGrid Control의 모습이 전보다는 좀 더 진화된 상태가 되었다.

다음에는 Row 선택 기능을 추가해 보기로 하자. 

 

지금까지의 소스는 아래를 참고하면 된다.

WANI Grid_20190725.zip
0.35MB

반응형
반응형

지금까지의 작업으로 Grid Control의 외적인 모습들을 개략적으로 만들어 왔다.

오늘은 Grid Control에 남아 있는 세로 스크롤바 이벤트 처리와 마우스 휠 이벤트 처리를 구현해서 Grid Control에서 정보를 Viewing하기 위한 일반적인 방법들을 구현하고자 한다.

 

저자가 작성한 소스를 본 사람들은 알겠지만 다소 엉성한 부분들이 꽤 있을 수 있다. 하나 하나의 기능을 만들어가면서 내용을 정리하고 있는 수준이다 보니 많은 부분이 깔끔하지 못할 수 있지만 나 처럼 누군가가 만들어 놓은 Grid Control을 사용만 해오다가 직접 내가 이러한 Control을 만들어 보면서 하나씩 배우고 익히는 부분이기에 더 그럴 수 있을 것이다. 

저자가 구현한 부분에 보다 좋은 아이디어나 방법이 있다면 댓글로 많은 조언과 도움을 주시길 희망하며...

 

[그림 10-1] 행 추가를 통해서 세로 스크롤바 및 마우스 휠 사용

세로 스크롤바의 경우에는 행(Row)의 맨 처음과 마지막 까지 스크롤바의 움직임에 따라 행(Row)을 이동하면서 행의 컬럼들을 보여주게 된다.

WANIGrid Control 내에서 세로스크롤바가 처음 나타났을 때의 행(Row) 개수를 파악하고 저장하기 위한 rowsCount 변수를 선언한다.

private int rowsCount = 0;  //Grid Control에서 보여지는 행의 갯수를 저장하기 위한 변수

 

WANIGrid Control의 사이즈가 변경될 때, Control 내의 표기할 수 있는 행(Row)의 개수가 변경될 수 있기에 rowsCount 변수를 초기화 하는 부분을 추가한다.

HScrollBar/VScrollBar를 초기화 하는 메소드에 rowsCount를 초기화한다.

public void InitializeScollBar()
{
    rowsCount = 0; //Control 크기가 변경되었을 경우 행(Row) 개수 초기화

    //가로 스크롤바 설정
    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;        
}

 

WANIGrid Control의 폭/높이를 기준으로 가로/세로 스크롤바를 보여줄지를 결정하는 메소드인 ReCalcScrollBar에서 rowsCount 변수에 Row개수를 설정하는 부분을 추가한다.

WANIGrid Control에서 세로 스크롤바가 나타나기 전의 Row수를 설정하는 부분을 추가해서 세로 스크롤바 또는 마우스 휠 이벤트 처리 시에 사용할 수 있도록 한다.

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의 첫번째 컬럼 부터 그려지도록 처리
    }
    //로우의 높이가 클라이언트 사이즈 높이 보다 클 경우 세로 스크롤바를 보여준다.
    if (allRowsHeight > 0 && (allRowsHeight > Height - topHeaderHeight - xsclHeight))
    {
        vScrollBar.Visible = true;
        if (rowsCount == 0) rowsCount = (allRowsHeight / rowHeight) - 1; //WANIGrid Control에서 세로 스크롤바가 나타나기 전의 Row 개수 설정
        vScrollBar.Maximum = allRowsHeight;
        vScrollBar.LargeChange = rowHeight * 5;
        vScrollBar.SmallChange = rowHeight;
    }
    else
    {
        vScrollBar.Visible = false;
        grid.FirstVisibleRow = 0;
    }
}

 

세로 스크롤바를 움직였을 때 발생하는 이벤트

private void VScrollBar_Scroll(object sender, ScrollEventArgs e)
{
    firstVisibleRow = e.NewValue / rowHeight; //입력되는 세로 스크롤바의 값을 Row 위치로 변환

    if (firstVisibleRow > (allRowsHeight / rowHeight)) return;
    if (firstVisibleRow >= (allRowsHeight / rowHeight) - rowsCount)
    {
        firstVisibleRow = (allRowsHeight / rowHeight) - rowsCount;
        grid.FirstVisibleRow = firstVisibleRow;
        vScrollBar.Value = vScrollBar.Maximum;                
    }
    else
    {
        grid.FirstVisibleRow = firstVisibleRow;
        vScrollBar.Value = firstVisibleRow * rowHeight;
    }
                        
    CalcVisibleRange();
    ReCalcScrollBars();
    Invalidate();
}

 

마우스 휠 처리는 컨트롤 키를 누르고 휠을 움직였을 때와 그렇지 않고 휠만 움직였을 떄로 구분해서 처리한다.

컨트롤 키를 누른 상태에서 휠을 위/아래로 움직이면 가로 스크롤바를 움직인 효과가 나도록 했고, 마우스 휠만 위/아래로 움직였을 경우에는 세로 스크롤바를 움직인 효과가 나도록 처리했다.

마우스 휠의 경우 위로 굴리면 양수 값이 아래로 굴리면 음수 값이 나오게 된다. 

컨트롤 키를 누른 상태에서 마우스 휠을 위로 굴리면 가로 스크롤의 좌측으로, 아래로 굴리면 우측으로 움직이는 효과가 나도록 처리했다.

마우스 휠을 위로 굴리면 세로 스크롤의 위쪽으로, 아래로 굴리면 세로 스크롤의 아래쪽으로 움직인 효과가 나도록 처리했다.

private void Mouse_Wheel(object sender, MouseEventArgs e)
{
    //Control Key를 누르고 Wheel을 돌렸을 경우는 HScrollBar와 동일
    if (ModifierKeys == Keys.Control)
    {
        if ((e.Delta / 120) > 0)  //업의 경우에는 좌측으로 이동
        {
            firstVisibleCol -= 2;
            if (firstVisibleCol < 0) firstVisibleCol = 0;
            grid.FirstVisibleCol = firstVisibleCol;
            hScrollBar.Value = firstVisibleCol;
        }
        else //다운의 경우에는 우측으로 이동
        {
            firstVisibleCol += 2;
            if (firstVisibleCol >= (grid.GridHeaderList.Count - 1)) firstVisibleCol = grid.GridHeaderList.Count - 3;
            grid.FirstVisibleCol = firstVisibleCol;
            hScrollBar.Value = hScrollBar.Maximum;
        }
    }
    else //Wheel만 움직였을 경우에는 VScrollBar와 동일
    {
        if (firstVisibleRow < 0) return; 

        if ((e.Delta / 120) > 0) //업의 경우 위쪽으로 이동
        {
            firstVisibleRow -= 2;
            if (firstVisibleRow < 0)
            {
                firstVisibleRow = 0;
                grid.FirstVisibleRow = firstVisibleRow;
                vScrollBar.Value = 0;
            }
            else
            {
                grid.FirstVisibleRow = firstVisibleRow;
                vScrollBar.Value = firstVisibleRow * rowHeight;
            }
        }
        else //다운의 경우에는 아래쪽으로 이동
        {
            if (rowsCount == 0) return; // rowsCount가 0일 경우는 Row의 Height가 Control Height를 넘지 않았음
            firstVisibleRow += 2;
            if (firstVisibleRow >= (allRowsHeight / rowHeight) - rowsCount)
            {
                firstVisibleRow = (allRowsHeight / rowHeight) - rowsCount;
                grid.FirstVisibleRow = firstVisibleRow;
                vScrollBar.Value = vScrollBar.Maximum;
            }
            else
            {
                grid.FirstVisibleRow = firstVisibleRow;
                vScrollBar.Value = firstVisibleRow * rowHeight;
            }
        }
    }
    CalcVisibleRange();
    ReCalcScrollBars();
    Invalidate();
}

 

이렇게 해서 지금까지는 Grid Header를 만들고, Row를 추가하고 가로/세로 스크롤바 및 마우스 휠 이벤트 처리까지 구현을 해 보았다.

 

추가 한 행(Row)에 직접 데이터를 입력하고 보여주는 부분을 구현해 나가기에 앞서 WANIGrid Control 내에서 마우스로 클릭한 부분의 좌표를 Grid 내의 Cell 선택 영역으로 표시하기 위한 기능을 구현할 생각이다.

사용자가 WANIGrid Control 내의 Header 영역을 제외한 부분을 클릭하면 클릭 된 영역이 어느 행과 컬럼인지를 식별하기 위한 메소드를 만들어 볼 것이다.

부족하지만 Grid Control에 필요한 기능들을 고민과 학습을 통해서 하나씩 하나씩 천천히 구현해 나가볼 생각이다.

지금까지의 소스는 아래와 같다.

WANI Grid_20190721.zip
0.33MB

저자의 부족한 소스를 보고 조언 및 더 나은 방법이 있다면 기탄없이 댓글을 달아주시길 희망해 본다.

여러분들이 보내주는 의견들이 곧 WANIGrid Control의 기능을 보다 편리하고 고도화해 나갈 수 있는 방법이라 생각한다.

 

 

반응형
반응형

지금까지 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

 

 

 

반응형

+ Recent posts