星期六, 8月 24, 2024

[C#] 自訂資料列外觀

參考官方文章 - How to: Customize the Appearance of Rows in the Windows Forms DataGridView Control 來自訂 DataGridViewRow 外觀,繪製 DataGrdiViewRow 外觀要使用 RowPrePaint 和 RowPostPaint 事件,該範例在
  • RowPrePaint 事件繪製 DataGridViewRow selection 漸層背景顏色
  • RowPostPaint 事件繪製 DataGridViewRow 跨欄位文字內容
C# Code
using System.Drawing.Drawing2D;

namespace RowPaintingSample
{
    public partial class Form1 : Form
    {
        private int _oldRowIndex { get; set; } = 0;
        private int _custom_Content_Height { get; } = 30;
        private int _hideColumnIndex { get; } = 2;
        private DataGridViewAutoSizeRowMode _defaultDataGridViewAutoSizeRowMode { get; } = DataGridViewAutoSizeRowMode.AllCellsExceptHeader;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

            // 設定 Padding 和 Height 讓 Row 下方有空間繪製跨欄文字
            Padding newPadding = new Padding(0, 1, 0, _custom_Content_Height);
            dataGridView1.RowTemplate.DefaultCellStyle.Padding = newPadding;
            dataGridView1.RowTemplate.Height += _custom_Content_Height;

            // 顏色設定為 Transparent,才不會覆蓋自行繪製的 Selection 背景顏色
            dataGridView1.RowTemplate.DefaultCellStyle.SelectionBackColor = Color.Transparent;

            // 設定 DataGridView 相關屬性
            dataGridView1.AllowUserToAddRows = false;
            dataGridView1.CellBorderStyle = DataGridViewCellBorderStyle.None;
            dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;

            // 設定 DataGridViewColumnHeader 文字
            dataGridView1.ColumnCount = 4;
            dataGridView1.Columns[0].Name = "Recipe";
            dataGridView1.Columns[0].SortMode = DataGridViewColumnSortMode.NotSortable;
            dataGridView1.Columns[1].Name = "Category";
            dataGridView1.Columns[_hideColumnIndex].Name = "Main Ingredients";
            dataGridView1.Columns[3].Name = "Rating";

            // 該欄位資料要繪製至 DataGridViewRow 內跨欄顯示
            dataGridView1.Columns[_hideColumnIndex].Visible = false;

            // Populate the rows of the DataGridView
            string[] row1 = new string[]{
                "Meatloaf",
                "Main Dish",
                "1 lb. lean ground beef, 1/2 cup bread crumbs, 1/4 cup ketchup, 1/3 tsp onion powder, 1 clove of garlic, 1/2 pack onion soup mix, dash of your favorite BBQ Sauce",
                "****"};

            string[] row2 = new string[]{
                "Key Lime Pie",
                "Dessert",
                "lime juice, whipped cream, eggs, evaporated milk",
                "****"};

            string[] row3 = new string[]{
                "Orange-Salsa Pork Chops",
                "Main Dish",
                "pork chops, salsa, orange juice, pineapple",
                "****"};

            string[] row4 = new string[]{
                "Black Bean and Rice Salad",
                "Salad",
                "black beans, brown rice",
                "****"};

            string[] row5 = new string[]{
                "Chocolate Cheesecake",
                "Dessert",
                "cream cheese, unsweetened chocolate",
                "***"};

            string[] row6 = new string[]{
                "Black Bean Dip",
                "Appetizer",
                "black beans, sour cream, salsa, chips",
                "***"};

            object[] rows = new object[] { row1, row2, row3, row4, row5, row6 };
            foreach (string[] rowArray in rows)
                dataGridView1.Rows.Add(rowArray);

            // 自行調整 DataGridViewRow 來顯示文字內容
            dataGridView1.AutoResizeRows(DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders);

            dataGridView1.RowPrePaint += dataGridView1_RowPrePaint;
            dataGridView1.RowPostPaint += dataGridView1_RowPostPaint;
            dataGridView1.ColumnWidthChanged += dataGridView1_ColumnWidthChanged;
            dataGridView1.RowHeightChanged += dataGridView1_RowHeightChanged;
            dataGridView1.CurrentCellChanged += dataGridView1_CurrentCellChanged;
        }

        // Column 寬度有變化時重新繪製
        void dataGridView1_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
        {
            var dgv = sender as DataGridView;
            dgv.Invalidate();
        }

        // Row 高度有變化時重新繪製
        void dataGridView1_CurrentCellChanged(object sender, EventArgs e)
        {
            var dgv = sender as DataGridView;

            if (_oldRowIndex != -1)
            {
                dgv.InvalidateRow(_oldRowIndex);
            }

            // CurrentCellAddress.X 代表 ColumnIndex
            // CurrentCellAddress.Y 代表 RowIndex
            _oldRowIndex = dgv.CurrentCellAddress.Y;
        }

        void dataGridView1_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
        {
            var dgv = sender as DataGridView;

            // 不自動繪製 focus 的方框
            e.PaintParts &= ~DataGridViewPaintParts.Focus;

            // 判斷該是否為使用者選取的 Row
            if (e.State.HasFlag(DataGridViewElementStates.Selected) == false)
                return;

            Rectangle rowBounds = GetRowBounds(dgv, e.RowBounds);

            // 繪製 selection 背景
            using Brush backbrush = new LinearGradientBrush(rowBounds,
                    dgv.DefaultCellStyle.SelectionBackColor,
                    e.InheritedRowStyle.ForeColor,
                    LinearGradientMode.Horizontal);

            e.Graphics.FillRectangle(backbrush, rowBounds);

            // 文件說明紀錄:e.Handled 設定為 true,CellPainting 和 RowPostPaint 事件就不會觸發
            // e.Handled = true;
        }

        // 跨欄位文字內容繪製
        void dataGridView1_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
        {
            var dgv = sender as DataGridView;

            Rectangle rowBounds = GetRowBounds(dgv, e.RowBounds);

            // 在這裡使用 forebrush 進行繪圖操作
            using SolidBrush forebrush =
                e.State.HasFlag(DataGridViewElementStates.Selected)
                ? new SolidBrush(e.InheritedRowStyle.SelectionForeColor)
                : new SolidBrush(e.InheritedRowStyle.ForeColor);

            // 取得隱藏欄位文字內容,Cells[2] 是隱藏欄位
            object recipe = dgv.Rows.SharedRow(e.RowIndex).Cells[_hideColumnIndex].Value;

            if (recipe != null)
            {
                string text = recipe.ToString();

                // Calculate the bounds for the content that spans multiple
                // columns, adjusting for the horizontal scrolling position
                // and the current row height, and displaying only whole
                // lines of text.
                Rectangle textArea = rowBounds;
                textArea.X -= dataGridView1.HorizontalScrollingOffset;
                textArea.Width += dataGridView1.HorizontalScrollingOffset;
                textArea.Y += rowBounds.Height - e.InheritedRowStyle.Padding.Bottom;
                textArea.Height -= rowBounds.Height - e.InheritedRowStyle.Padding.Bottom;
                textArea.Height = (textArea.Height / e.InheritedRowStyle.Font.Height) * e.InheritedRowStyle.Font.Height;

                // Calculate the portion of the text area that needs painting.
                RectangleF clip = textArea;
                clip.Width -= dgv.RowHeadersWidth + 1 - clip.X;
                clip.X = dgv.RowHeadersWidth + 1;

                // 原本的繪製區域
                RectangleF oldClip = e.Graphics.ClipBounds;

                // 限定繪製區域
                e.Graphics.SetClip(clip);

                // 繪製跨欄位文字內容
                e.Graphics.DrawString(text, e.InheritedRowStyle.Font, forebrush, textArea);

                // 回復原本的繪製區域
                e.Graphics.SetClip(oldClip);
            }

            if (dgv.CurrentCellAddress.Y == e.RowIndex)
            {
                // 繪製 focus 時的外框                
                e.DrawFocus(rowBounds, true);
            }
        }

        private Rectangle GetRowBounds(DataGridView dgv, Rectangle rowBounds)
        {
            // 計算 Row 邊界
            return new Rectangle(
                dgv.RowHeadersWidth,
                rowBounds.Top,
                dgv.Columns.GetColumnsWidth(DataGridViewElementStates.Visible) - dgv.HorizontalScrollingOffset + 1,
                rowBounds.Height);
        }

        // Adjusts the padding when the user changes the row height so that
        // the normal cell content is fully displayed and any extra
        // height is used for the content that spans multiple columns.
        void dataGridView1_RowHeightChanged(object sender, DataGridViewRowEventArgs e)
        {
            // 計算 Row 高度
            int preferredNormalContentHeight = e.Row.GetPreferredHeight(e.Row.Index, _defaultDataGridViewAutoSizeRowMode, true) - e.Row.DefaultCellStyle.Padding.Bottom;

            // 指定新的 Padding 設定值
            Padding newPadding = e.Row.DefaultCellStyle.Padding;
            newPadding.Bottom = e.Row.Height - preferredNormalContentHeight;
            e.Row.DefaultCellStyle.Padding = newPadding;
        }

    }
}

CurrentCellChanged

該範例是在 CurrentCellChanged 事件內判斷 Row 是否有變化,因為該範例有設定 SelectionMode 為 DataGridViewSelectionMode.FullRowSelect,搭配 SelectionChanged 事件也可以達成同樣效果,改寫如下
private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
    var dgv = sender as DataGridView;
    if (dgv == null ||
        dgv.CurrentRow == null)
        return;

    dgv.InvalidateRow(dgv.CurrentRow.Index);
}

繪製 Focus

該範例在 RowPrePaint 事件特別去移除繪製 Focus,RowPostPaint 事件才又指定繪製 Focus,因為 Selection 背景漸層顏色關係,完全看不出 Focus 效果,故意把 Selection 背景漸層顏色繪製拿掉來觀察,效果如下


點擊 Cell 後繪製變化

點擊進入 Cell 後,跨欄位文字內容就消失啦

操作效果

Column 寬度變化

Row 高度變化


該範例蠻多沒用過功能,有去了解並筆記

星期四, 8月 22, 2024

[C#] Deconstruct - 自訂型別

看見 ValueTuple 搭配 Deconstruct() 寫法來接回傳值,參考官方文章 Deconstructing tuples and other types 寫個簡易範例來記錄

定義 Employee Class
namespace DeconstructDemo
{
    public class Employee
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public string PhoneNumber { get; set; }

        public Employee(string name, int age, string phoenNumber)
        {
            Name = name;
            Age = age;
            PhoneNumber = phoenNumber;
        }

        // 參數必須宣告為 out
        public void Deconstruct(out string name, out int age, out string phoneNumber)
        {
            name = Name;
            age = Age;
            phoneNumber = PhoneNumber;
        }

        // Deconstruct 可以多載
        public void Deconstruct(out string name, out int age)
        {
            name = Name;
            age = Age;
        }
    }
}
實際使用
namespace DeconstructDemo
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Case1:一般寫法
            var result = new Employee("姓名 1", 18, "111111111");
            Console.WriteLine($"Case1:Name:{result.Name} - Age:{result.Age}");

            // Case2:Deconstruct()
            var (name2, age2, phoneNumber2) = new Employee("姓名 2", 28, "222222222");
            Console.WriteLine($"Case2:Name:{name2}");
            
            // Case3:Deconstruct() 多載
            var (name3, age3) = new Employee("姓名 3", 38, "333333333");
            Console.WriteLine($"Case3:Age:{age3}");
            
            // Case4:不使用 var,明確指定型別
            (string name4, int age4, string phoneNumber4) = new Employee("姓名 4", 48, "444444444");
            Console.WriteLine($"Case4:PhoneNumber:{phoneNumber4}");
        }
    }
}

星期六, 8月 17, 2024

[C#] Graphics.SetClip 方法

根據官方文件 - Graphics.SetClip 方法 來理解 SetClip() 如何使用
Sets the clipping region of this Graphics to the Clip property of the specified Graphics.
白話翻譯就是在 Graphics 上限定區域,只能繪製該區域,無法超過該區域
namespace GraphicsNETSample
{
    public partial class FrmSetClip : Form
    {
        public FrmSetClip()
        {
            InitializeComponent();
        }

        private void FrmSetClip_Paint(object sender, PaintEventArgs e)
        {
            SetClipRectangle(e);
        }

        private void SetClipRectangle(PaintEventArgs e)
        {
            // 繪製一個 300 X 300 矩形的灰色背景
            Rectangle bgRect = new Rectangle(0, 0, 300, 300);
            e.Graphics.FillRectangle(new SolidBrush(Color.LightGray), bgRect);

            // 限定在 Graphics 上繪製只能在該 100 X 100 矩形內
            Rectangle clipRect = new Rectangle(0, 0, 100, 100);
            e.Graphics.SetClip(clipRect);

            // 故意繪製一個同 300 X 300 灰色背景的矩形範圍
            // 但因為 Graphics 已經被限定在 100 X 100 矩形範圍內,
            // 所以只會繪製出 100 X 100 的紅色矩形
            e.Graphics.FillRectangle(new SolidBrush(Color.Red), bgRect);
        }
    }
}

星期四, 8月 15, 2024

[Azure] WorkItem - 移除指定使用者清單

當使用者從 Organization 移除後,Board WorkItem 指定使用者清單仍然會存在,在 Unable to remove previous employees from Azure DevOps Sprint Board Assignment drop-down 討論內找到解法,就故意去選擇該使用者,會出現下圖的錯誤訊息,然後離開 WorkItem 後再進入就會移除
The Identity value 'usename user@gamil.com' for field 'Assigned To' is an unknown identity

星期二, 8月 13, 2024

[C#] Enum - FlagsAttribute - Remove

在官方文章 - How to: Customize the Appearance of Rows in the Windows Forms DataGridView Control 內看到這段語法
e.PaintParts &= ~DataGridViewPaintParts.Focus;
個人筆記內有紀錄過 Enum Attribute 兩種移除方式
// 寫法一
var result = EnumSource ^ FlagRemove;

// 寫法二
var result = EnumSource & ~FlagRemove;

本篇筆記重點在於了解 EnumSource & ~FlagRemove; 和 EnumSource &= ~FlagRemove; 兩者差異
  • EnumSource & ~FlagRemove:不會移除 EnumSource 內的 FlagRemoveEnum,但回傳值是移除的
  • EnumSource &= ~FlagRemove:直接移除 EnumSource 內的 FlagRemoveEnum
寫個簡易範例來驗證
using System;

namespace EnumFlags
{
    class Program
    {
        static void Main(string[] args)
        {

            Days source = Days.Tuesday | Days.Thursday | Days.Saturday;
            Days flagRemove = Days.Thursday;
            Console.WriteLine($"初始值:{source}");

            // 使用 source & ~flagRemove 不會改變 source
            Days result = source & ~flagRemove;
            Console.WriteLine($"source & ~flagRemove result 值:{result} "); // Output: Tuesday、Saturday
            Console.WriteLine($"source & ~flagRemove source 值:{source} "); // Output: Tuesday、Thursday、Saturday

            // 使用 source &= ~flagToRemove 直接改變 source
            source &= ~flagRemove;
            Console.WriteLine($"source &= ~flagToRemove source 值:{source}"); // Output: Tuesday、Saturday
        }
    }

    // 沒有特別宣告的話,Enum 預設是 int
    [Flags]
    public enum Days
    {
        Monday = 1,
        Tuesday = 2,
        Wednesday = 4,
        Thursday = 8,
        Friday = 16,
        Saturday = 32,
        Sunday = 64,

        All = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
    };
}

星期日, 8月 11, 2024

[C#] DataGridView 繪製行號

DataGridView 常見功能 - 在 RowHeader 上繪製行號,透過該練習來熟悉 RowPostPaint 事件DataGridViewRowPostPaintEventArgs


C# Code
namespace RowNumberPaint
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // 新增 1000 筆資料
            dataGridView1.RowCount = 1000;
            // 故意把 RowHeader 設定窄些
            dataGridView1.RowHeadersWidth = 30;
        }

        private void dataGridView1_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
        {
        
            var dgv = sender as DataGridView;
            
            if (dgv.RowHeadersVisible == false)
                return;

            string rowNumber = (e.RowIndex + 1).ToString();
            var font = dgv.RowHeadersDefaultCellStyle.Font;
            var brush = new SolidBrush(dgv.RowHeadersDefaultCellStyle.ForeColor);

            SizeF textSize = e.Graphics.MeasureString(rowNumber, font);
            // 文字置中
            float x = (dgv.RowHeadersWidth - textSize.Width) / 2;
            float y = e.RowBounds.Location.Y + ((e.RowBounds.Height - textSize.Height) / 2);

            e.Graphics.DrawString(rowNumber, font, brush, x, y);

            // 當文字超過 RowHeaders 寬度時,會自動延伸
            int width = (int)textSize.Width;
            if (dgv.RowHeadersWidth <= width)
                dgv.RowHeadersWidth += width;

        }
    }
}

MeasureString()

使用 MeasureString() 來測量文字寬高來達到兩個需求
  • 讓文字寬高都可以在 RowHeader 內置中
  • 判斷文字是否寬於 RowHeader,有超過要自動延伸

從下圖數字 9 到 10 可以觀察到行數有在 RowHeader 內置中
從下圖數字 99 到 100 可以觀察到 RowHeader 有自動延伸,橫向 ScrollBar 也出現啦

星期二, 8月 06, 2024

[C#] 繪製漸層

根據官方文章 - How to: Create a Linear Gradient 的筆記,了解如何使用 LinearGradientBrush 和 Blend

水平漸層

當水平座標超過 200 時,漸層會重複,把長度拉到 600,可以看見顏色重覆三次
private void UseHorizontalLinearGradients(PaintEventArgs e)
{
	using LinearGradientBrush linGrBrush = new LinearGradientBrush(
		new Point(0, 10),
		new Point(200, 10),
		Color.FromArgb(255, 255, 0, 0),  // Start Color:Opaque Red
		Color.FromArgb(255, 0, 0, 255)); // End Color:Opaque Blue
	
	e.Graphics.FillRectangle(linGrBrush, 0, 5, 600, 30);
}


自訂水平漸層

利用 Blend 來更細緻自訂漸層
private void CustomizeLinearGradients(PaintEventArgs e)
{
	using LinearGradientBrush linGrBrush = new LinearGradientBrush(
		new Point(0, 10),
		new Point(200, 10),
		Color.FromArgb(255, 255, 0, 0),  // Start Color:Opaque Red
		Color.FromArgb(255, 0, 0, 255)); // End Color:Opaque Blue
	
	float[] factors = { 0.2f, 0.4f, 0.8f, 0.8f, 0.4f, 0.2f };
	float[] positions = { 0.0f, 0.2f, 0.4f, 0.6f, 0.8f, 1.0f };
	
	Blend blend = new Blend();
	blend.Factors = factors;
	blend.Positions = positions;
	linGrBrush.Blend = blend;
	
	e.Graphics.FillRectangle(linGrBrush, 0, 45, 600, 30);
}

Blend 兩大重點屬性:[Factor-顏色比例] 和  [Position-顏色位置],兩者值皆介於 0.0f 和 1.0f 之間且兩者必須成對設定。

[Factor-顏色比例]:該筆記是設定 Red 到 Blue 漸層,來驗證看看文件上的說明
private void FactorsDemo(PaintEventArgs e)
{
	using LinearGradientBrush linGrBrush = new LinearGradientBrush(
		new Point(0, 10),
		new Point(200, 10),
		Color.FromArgb(255, 255, 0, 0),  // Start Color:Opaque Red
		Color.FromArgb(255, 0, 0, 255)); // End Color:Opaque Blue

	float[] factors = { 0.0f, 0.5f, 1.0f }; // 最左邊全紅色、最右邊全藍色
	float[] positions = { 0.0f, 0.5f, 1.0f };
	
	Blend blend = new Blend();
	blend.Factors = factors;
	blend.Positions = positions;
	linGrBrush.Blend = blend;
	
	e.Graphics.FillRectangle(linGrBrush, 0, 5, 200, 30);
}
[Position-顏色位置]:多設定值時,起始值 0.0f 和 結束值 1.0f 必須存在,要不然會拋出 Exception 的
完整 C# Code
using System.Drawing.Drawing2D;

namespace GraphicsNETSample
{
    public partial class FrmLinearGradient : Form
    {
        public FrmLinearGradient()
        {
            InitializeComponent();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            UseHorizontalLinearGradients(e);
            CustomizeLinearGradients(e);
        }

        private LinearGradientBrush GetLinearGradientBrush()
        {
            return new LinearGradientBrush(
            new Point(0, 10),
            new Point(200, 10),
            Color.FromArgb(255, 255, 0, 0),
            Color.FromArgb(255, 0, 0, 255));
        }

        private void UseHorizontalLinearGradients(PaintEventArgs e)
        {
            using LinearGradientBrush linGrBrush = GetLinearGradientBrush();

            e.Graphics.FillRectangle(linGrBrush, 0, 5, 600, 30);
        }

        private void CustomizeLinearGradients(PaintEventArgs e)
        {
            using LinearGradientBrush linGrBrush = GetLinearGradientBrush();

            float[] factors = { 0.2f, 0.4f, 0.8f, 0.8f, 0.4f, 0.2f };
            float[] positions = { 0.0f, 0.2f, 0.4f, 0.6f, 0.8f, 1.0f };

            Blend blend = new Blend();
            blend.Factors = factors;
            blend.Positions = positions;
            linGrBrush.Blend = blend;

            e.Graphics.FillRectangle(linGrBrush, 0, 45, 600, 30);
        }
    }
}

星期一, 8月 05, 2024

[CCNA] Cisco Packet Tracer - 遠端連線 SSH

[CCNA] Cisco Packet Tracer - 遠端連線 內筆記 Console 和 Telnet,該篇則是使用 SSH 來進行遠端連線
Switch0 會需要先建立 IP、HostName、DomainName 和啟用 Enabled 密碼

在Switch 上啟用 SSH

執行 crypto key generate rsa 前要先設定 HostName 和 DomainName 才行,要不然會被防呆下來

tranport input 限定只能使用 SSH 連線

完整指令

設定 enable 密碼

星期日, 8月 04, 2024

[C#] 擴充儲存格行為

根據官方文章 - How to: Customize Cells and Columns in the Windows Forms DataGridView Control by Extending Their Behavior and Appearance 筆記,該內容是當滑鼠移進 DataGridViewCell 時會繪製一個紅色框線,滑出時消失



DataGridViewRolloverCell

從 DataGridViewCell 去呼叫 DataGridView 功能 (PointToClient() 和 InvalidateCell()),內部呼叫外部控件函數,IDE 一直有 null 警告,畢竟開發當下沒有放進 DataGridView 內,理論上是 null 沒錯
namespace ExtendingBehavior
{
    public class DataGridViewRolloverCell : DataGridViewTextBoxCell
    {
        protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
        {
            // 繪製預設外觀
            base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);

            // 取得滑鼠在 DataGridView 內的位置
            Point cursorPosition = DataGridView.PointToClient(Cursor.Position);

            // 如果滑鼠在 Cell 範圍內就繪製紅框
            if (cellBounds.Contains(cursorPosition))
            {
                Rectangle newRect = new Rectangle(
                    cellBounds.X + 1,
                    cellBounds.Y + 1,
                    cellBounds.Width - 4,
                    cellBounds.Height - 4);

                graphics.DrawRectangle(Pens.Red, newRect);
            }
        }

        protected override void OnMouseEnter(int rowIndex)
        {
            DataGridView.InvalidateCell(this);
        }

        protected override void OnMouseLeave(int rowIndex)
        {
            DataGridView.InvalidateCell(this);
        }
    }
}

DataGridViewRolloverCellColumn

單純指定 CellTemplate 而已
namespace ExtendingBehavior
{
    public class DataGridViewRolloverCellColumn : DataGridViewColumn
    {
        public DataGridViewRolloverCellColumn()
        {
            CellTemplate = new DataGridViewRolloverCell();
        }
    }
}

主程式

根據文件塞 10 筆資料進去 DataGridView 
namespace ExtendingBehavior
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            for (int i = 0; i < 10; i++)
                dataGridView1.Rows.Add(new string[] { "" });
        }
    }
}
文件內有這段文字說明
This example will not work correctly if you add empty rows. Empty rows are created, for example, when you add rows to the control by setting the RowCount property. This is because the rows added in this case are automatically shared, which means that DataGridViewRolloverCell objects are not instantiated until you click on individual cells, thereby causing the associated rows to become unshared.
改成使用 RowCount 來新增資料,驗證看看到底會是甚麼情況
dataGridView1.RowCount = 10;

星期六, 8月 03, 2024

[C#] 自訂儲存格外觀

根據官方文件 - 如何:在 Windows Form DataGridView 控制項中自訂儲存格的外觀 來練習,除了文章內容外,另外也新增自訂欄位來驗證 GridLine


C# Code

繪製步驟依循下述步驟
  • 繪製背景顏色
  • 繪製 GridLine
  • 繪製自訂框線
  • 繪製文字內容
兩欄位使用函數差異在於
  • 繪製 GridLine:官方 DrawLine()、自訂 PaintBackground()
  • 繪製文字內容:官方 DrawString()、自訂 PaintContent()
namespace CellPaintingEvent
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            dataGridView1.RowCount = 5;
            for (int i = 0; i < dataGridView1.RowCount; i++)
            {
                dataGridView1.Rows[i].Cells[ColContactName.Index].Value = $"Contact-{i}";
                dataGridView1.Rows[i].Cells[ColCustom.Index].Value = $"自訂內容-{i}";
            }
        }

        private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
        {
            if (e.RowIndex < 0)
                return;

            using Brush backColorBrush = new SolidBrush(e.CellStyle.BackColor);

            if (dataGridView1.Columns[ColContactName.Index].Index == e.ColumnIndex)
            {
                using Pen gridLinePen = new Pen(dataGridView1.GridColor);

                // Erase the cell.
                e.Graphics.FillRectangle(backColorBrush, e.CellBounds);

                // Draw the grid lines(only the right and bottom lines;
                // DataGridView takes care of the others).
                e.Graphics.DrawLine(gridLinePen,
                    e.CellBounds.Left,
                    e.CellBounds.Bottom - 1,
                    e.CellBounds.Right - 1,
                    e.CellBounds.Bottom - 1);

                e.Graphics.DrawLine(gridLinePen,
                    e.CellBounds.Right - 1,
                    e.CellBounds.Top,
                    e.CellBounds.Right - 1,
                    e.CellBounds.Bottom);

                // Draw the inset highlight box.
                Rectangle newRect = new Rectangle(
                    e.CellBounds.X + 1,
                    e.CellBounds.Y + 1,
                    e.CellBounds.Width - 4,
                    e.CellBounds.Height - 4);

                e.Graphics.DrawRectangle(Pens.Blue, newRect);

                // Draw the text content of the cell, ignoring alignment.
                if (e.Value != null)
                {
                    e.Graphics.DrawString(
                        (string)e.Value,
                        e.CellStyle.Font,
                        Brushes.Crimson,
                        e.CellBounds.X + 2,
                        e.CellBounds.Y + 2,
                        StringFormat.GenericDefault);
                }

                e.Handled = true;
                return;
            }


            if (dataGridView1.Columns[ColCustom.Index].Index == e.ColumnIndex)
            {
                // Step1:繪製背景顏色
                e.Graphics.FillRectangle(backColorBrush, e.CellBounds);

                // Step2:繪製 GridLine
                e.PaintBackground(e.ClipBounds, true);

                // Step3:繪製自訂框線
                if (e.RowIndex % 2 == 0)
                {
                    using Pen alarmPen = new Pen(Color.LightCoral, 3);

                    Rectangle rect = new Rectangle(
                        e.CellBounds.X,
                        e.CellBounds.Y,
                        e.CellBounds.Width - 2,
                        e.CellBounds.Height - 2);

                    e.Graphics.DrawRectangle(alarmPen, rect);
                }

                // Step4:繪製文字內容
                if (e.Value != null)
                    e.PaintContent(e.ClipBounds);

                e.Handled = true;
                return;
            }
        }
    }
}

GridLine

文件上特別提到 GridLine 只要繪製右方和下方線條就行,自訂外框欄位上特別用內建函數 PaintBackground() 來驗證看看。下述驗證語法為只在偶數列執行 PaintBackground() 來進行繪製,PaintBackground() 不會繪製背景,只有 GridLine 而已喔
if (dataGridView1.Columns[ColCustom.Index].Index == e.ColumnIndex)
{
    if (e.RowIndex % 2 != 0)
        e.PaintBackground(e.ClipBounds, true);

    e.Handled = true;
    return;
}
下圖官方欄位不繪製 GridLine,而自訂欄位只繪製偶數列 GridLine

星期五, 8月 02, 2024

[Win] Adobe Reader - 列印

一直都覺得現在 Edge 就可以開 PDF,就沒有刻意在新 PC 上安裝 Adobe Reader 來使用,這次幫同事換新 PC 後,被告知 PDF 沒有辦法多檔案列印,Edge 沒有下圖的列印功能



星期四, 8月 01, 2024

[C#] 停用 Windows Form DataGridView 控制項按鈕資料行中的按鈕

根據官方文章 - 停用 Windows Form DataGridView 控制項按鈕資料行中的按鈕 的練習筆記,文章內是把自訂 DataGridViewButtonCell 有 Disable 功能,則是再把 Visible 功能加進去

DataGridViewDisableButtonCell

要讓 DataGridViewButtonColumn 有無法使用或是隱藏效果,其實是透過 Paint 繪製 DataGridViewButtonColumn 外觀去變化,DataGridViewButtonCell 內會自訂 CellEnabled 和 CellVisible 來開放控制,Paint 事件內根據這兩個 Property 去判斷如何繪製外觀,另外因為是從繼承 DataGridViewButtonCell,所以要覆寫 Clone 並把 CellEnabled 和 CellVisible 塞進去,文件上也有特別說明該點
When you derive from DataGridViewCell or DataGridViewColumn and add new properties to the derived class, be sure to override the Clone method to copy the new properties during cloning operations. You should also call the base class's Clone method so that the properties of the base class are copied to the new cell or column.
using System.Windows.Forms.VisualStyles;

namespace DataGridViewDisableButtonColumnSample
{
    public class DataGridViewDisableButtonCell : DataGridViewButtonCell
    {
        public bool CellEnabled { get; set; } = true;

        public bool CellVisible { get; set; } = true;

        public DataGridViewDisableButtonCell()
        {
            // 預設值
            CellEnabled = true;
            CellVisible = true;
        }

        protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object? value, object? formattedValue, string? errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
        {
            // 計算 Cell 內繪製 Button 區域
            Rectangle buttonArea = cellBounds;
            Rectangle buttonAdjustment = BorderWidths(advancedBorderStyle);
            buttonArea.X += buttonAdjustment.X;
            buttonArea.Y += buttonAdjustment.Y;
            buttonArea.Height -= buttonAdjustment.Height;
            buttonArea.Width -= buttonAdjustment.Width;

            using SolidBrush cellBackground = new SolidBrush(cellStyle.BackColor);

            if (CellVisible == false)
            {
                // 把整個背景 Cell 都繪製為背景色,讓使用者以為沒有 Button 控件
                graphics.FillRectangle(cellBackground, buttonArea);
            }
            else if (CellEnabled == false)
            {
                if (paintParts.HasFlag(DataGridViewPaintParts.Background))
                    graphics.FillRectangle(cellBackground, cellBounds);

                if (paintParts.HasFlag(DataGridViewPaintParts.Border))
                    PaintBorder(graphics, clipBounds, cellBounds, cellStyle, advancedBorderStyle);

                ButtonRenderer.DrawButton(graphics, buttonArea, PushButtonState.Disabled);

                if (FormattedValue is string)
                {
                    TextRenderer.DrawText(graphics,
                        (string)FormattedValue,
                        DataGridView.Font,
                        buttonArea,
                        SystemColors.GrayText);
                }
            }
            else
            {
                base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
            }
        }

        public override object Clone()
        {
            DataGridViewDisableButtonCell cell = (DataGridViewDisableButtonCell)base.Clone();
            cell.CellEnabled = CellEnabled;
            cell.CellVisible = CellVisible;
            return cell;
        }
    }
}

DataGridViewDisableButtonColumn

很單純把 CellTemplate 指定使用 DataGridViewDisableButtonCell 而已       
namespace DataGridViewDisableButtonColumnSample
{
    public class DataGridViewDisableButtonColumn : DataGridViewButtonColumn
    {
        public DataGridViewDisableButtonColumn()
        {
            CellTemplate = new DataGridViewDisableButtonCell();
        }
    }
}  

主程式

觸發 CellValueChanged 事件,要特別指定重新繪製 DataGridViewButtonColumn 外觀,可以使用
  • Invalidate()
  • InvalidateColumn()
  • Refresh()
以上都可以達到重新繪製 DataGridViewButtonColumn 目的,該筆記是從最小單位繪製去思考,所以使用 InvalidateColumn()
namespace DataGridViewDisableButtonColumnSample
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            dataGridView1.AutoSize = true;
            dataGridView1.AllowUserToAddRows = false;
            dataGridView1.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;

            dataGridView1.RowCount = 8;
            for (int i = 0; i < dataGridView1.RowCount; i++)
                dataGridView1.Rows[i].Cells[ColButtonEnabled.Index].Value = $"Button{i}";

            dataGridView1.CellValueChanged += DataGridView1_CellValueChanged;
            dataGridView1.CurrentCellDirtyStateChanged += DataGridView1_CurrentCellDirtyStateChanged;
            dataGridView1.CellClick += DataGridView1_CellClick;
        }

        private void DataGridView1_CellClick(object? sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex != ColButtonEnabled.Index)
                return;

            DataGridViewDisableButtonCell buttonEnabled = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonEnabled.Index];
            if (buttonEnabled.CellEnabled == false)
                return;

            MessageBox.Show($"{buttonEnabled.Value} 是 Enabled");
        }

        private void DataGridView1_CurrentCellDirtyStateChanged(object? sender, EventArgs e)
        {
            if (dataGridView1.IsCurrentCellDirty &&
                dataGridView1.CurrentCell.ColumnIndex == ColChecked.Index)
                // CommitEdit() 會維持在編輯模式,並立即觸發 CellValueChanged 事件
                dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
        }

        private void DataGridView1_CellValueChanged(object? sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex != ColChecked.Index)
                return;

            bool value = (bool)dataGridView1.Rows[e.RowIndex].Cells[ColChecked.Index].Value;

            DataGridViewDisableButtonCell buttonEnabled = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonEnabled.Index];
            buttonEnabled.CellEnabled = !value;

            DataGridViewDisableButtonCell buttonVisible = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonVisible.Index];
            buttonVisible.CellVisible = !value;

            dataGridView1.InvalidateColumn(ColButtonEnabled.Index);
            dataGridView1.InvalidateColumn(ColButtonVisible.Index);
        }
    }
}