7.案例 - MetroButton

前面的几篇文章中介绍了一些自定义控件开发过程中的基本 这一篇主要是针对前面的两篇做一个案例 就做一个Metro风格的Button吧:

从左到右风别是控件的 默认状态-鼠标悬浮-鼠标点下-获得焦点及ALT点下-禁用

代码很简单 一共也就几十行:

(注意由于为了代码简洁 所以里面颜色什么的 我直接定死的 因为要不然一会代码贴上来 篇幅就太长了 如果你自己要写的话 还是尽量把那些定义成属性 让用户自己选择)

首先来完成基本的绘制:

protected override void OnPaint(PaintEventArgs e) {
    Graphics g = e.Graphics;

    StringFormat sf = new StringFormat();
    sf.Alignment = StringAlignment.Center;
    sf.LineAlignment = StringAlignment.Center;
    sf.HotkeyPrefix = this.ShowKeyboardCues ? HotkeyPrefix.Show : HotkeyPrefix.Hide;

    g.FillRectangle(Brushes.Black, this.ClientRectangle);

    if (this.ShowFocusCues && this.ContainsFocus) {     //绘制焦点框
        using (Pen p = new Pen(Color.Gray)) {
            p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
            g.DrawRectangle(p, 2, 2, this.Width - 5, this.Height - 5);
        }
    }
    g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, sf);
    base.OnPaint(e);
}
这样程序一运行起来 就会看到一个黑色的按钮和白色的文字 虽然上面有写绘制焦点框的代码 但是当控件得到或者失去焦点的时候 并不一定就立马有效果 因为没有谁去通知它重绘 所以还得绑定Got/LostFocus事件:
public MetroBtn() {
    this.GotFocus += (s, e) => this.Invalidate();
    this.LostFocus += (s, e) => this.Invalidate();
}

这样一来 基本的绘制就差不多了 现在还需要鼠标移动上去和点击下去的时候的效果

首先来看鼠标移动上去的时候的效果 要怎么样才能知道鼠标放到控件或者离开控件了?MouseEnter和MouseLeave事件就可以知道了 但是还需要一个标志来确定 当前是否鼠标在控件上 好在Paint的时候进行判断 然后做处理 然后我声明了一个变量来表示m_bMouseOn(名字而已 自己取名)然后绘制的时候就可以这样判断:

if (m_bMouseOn) {    //如果鼠标在控件上
    g.FillRectangle(Brushes.SlateBlue, this.ClientRectangle);
} else {             //否则依然黑色背景
    g.FillRectangle(Brushes.Black, this.ClientRectangle);
}
绘制中的判断处理完了 不过还得处理一下MouseEnter/MouseLeave去修改m_bMouseOn这个标识 并且通知重绘:
protected override void OnMouseEnter(EventArgs e) {
    m_bMouseOn = true;
    this.Invalidate();
    this.Focus();   //随你切换不切换焦点Focus()会自动触发paint的 所一有这行上面一行可以不要
    base.OnMouseEnter(e);
}

protected override void OnMouseLeave(EventArgs e) {
    m_bMouseOn = false;
    this.Invalidate();
    base.OnMouseLeave(e);
}
这样鼠标移动上去和离开的时候就能看到效果了 然后是处理鼠标点下的时候 依然声明一个变量标识m_bMouseDown然后去处理MouseDown/MouseUp事件:
protected override void OnMouseDown(MouseEventArgs e) {
    m_bMouseDown = true;
    this.Invalidate();  
    base.OnMouseDown(e);
}

protected override void OnMouseUp(MouseEventArgs e) {
    m_bMouseDown = false;
    this.Invalidate();
    base.OnMouseUp(e);
}
然后依然在Paint里面判断:
if (m_bMouseDown) {         //鼠标点下
    g.FillRectangle(Brushes.DarkSlateBlue, this.ClientRectangle);
} else if (m_bMouseOn) {    //注意MouseDown应该优先于MouseOn判断
    g.FillRectangle(Brushes.SlateBlue, this.ClientRectangle);
} else {
    g.FillRectangle(Brushes.Black, this.ClientRectangle);
}
但是这里要注意 应该先处理鼠标点下 因为鼠标点下必然鼠标是在控件上面 如果把第二个判断放上面去 那么就会悲剧

最后就是绘制控件禁用时候的外观了 这里我是单独在Paint外面写的方法去绘制的:

protected virtual void DrawDisable(Graphics g, StringFormat sf) {
    g.FillRectangle(Brushes.LightGray, this.ClientRectangle);
    g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, sf);
    g.TranslateTransform(-1, -1);
    g.DrawString(this.Text, this.Font, Brushes.Gray, this.ClientRectangle, sf);
    g.ResetTransform();
}
可以看到一来我就直接填充了一个浅灰色的颜色 然后绘制了两次文本 第一次是直接白色 然后把坐标系像左上角偏移了一个像素 然后再用灰色绘制了一次文字 这样看上去就会有一种阴影的效果

然后在Paint里面判断控件是否禁用 如果禁用就调用绘制:

if (!this.Enabled) {    //绘制禁用时候外观
    this.DrawDisable(g, sf);
    base.OnPaint(e);
    return; //如果中途要返回 记得调用base 不然用户绑定的Paint事件不会触发
}
绘制到此就差不多了 然后是处理一些焦点的点击和热键点下的时候:
//当点下回车或者空格的时候 默认触发点击事件
protected override void OnKeyDown(KeyEventArgs e) {
    if (e.Modifiers == Keys.None 
        && (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)) {
        this.OnClick(new EventArgs());
    }
    base.OnKeyDown(e);
}
//当热键被按下的时候 默认触发点击事件
protected override bool ProcessMnemonic(char charCode) {
    if (this.Enabled && this.Visible && IsMnemonic(charCode, this.Text)) {
        this.OnClick(new EventArgs());
        this.Focus();//随你要不 貌似系统的button热键点击的时候 焦点没有切换
        return true;
    }
    return base.ProcessMnemonic(charCode);
}
代码到此也就基本结束了 现在我把代码全部贴上来:
public class MetroBtn : Control
{
    public MetroBtn() {
        this.GotFocus += (s, e) => this.Invalidate();
        this.LostFocus += (s, e) => this.Invalidate();
    }

    private bool m_bMouseOn;    //鼠标是在控件上
    private bool m_bMouseDown;  //鼠标是否按下

    public override string Text {
        get { return base.Text; }
        set {
            if (base.Text == value) return;
            base.Text = value;
            this.Invalidate();
        }
    }

    protected override void OnMouseEnter(EventArgs e) {
        m_bMouseOn = true;
        this.Invalidate();
        base.OnMouseEnter(e);
    }

    protected override void OnMouseLeave(EventArgs e) {
        m_bMouseOn = false;
        this.Invalidate();
        base.OnMouseLeave(e);
    }

    protected override void OnMouseDown(MouseEventArgs e) {
        m_bMouseDown = true;
        this.Invalidate();
        this.Focus();   //随你切换不切换焦点Focus()会自动触发paint的 所一有这行上面一行可以不要
        base.OnMouseDown(e);
    }

    protected override void OnMouseUp(MouseEventArgs e) {
        m_bMouseDown = false;
        this.Invalidate();
        base.OnMouseUp(e);
    }
    //当点下回车或者空格的时候 默认触发点击事件
    protected override void OnKeyDown(KeyEventArgs e) {
        if (e.Modifiers == Keys.None 
            && (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)) {
            this.OnClick(new EventArgs());
        }
        base.OnKeyDown(e);
    }
    //当热键被按下的时候 默认触发点击事件
    protected override bool ProcessMnemonic(char charCode) {
        if (this.Enabled && this.Visible && IsMnemonic(charCode, this.Text)) {
            this.OnClick(new EventArgs());
            this.Focus();//随你要不 貌似系统的button热键点击的时候 焦点没有切换
            return true;
        }
        return base.ProcessMnemonic(charCode);
    }

    protected override void OnPaint(PaintEventArgs e) {
        Graphics g = e.Graphics;

        StringFormat sf = new StringFormat();
        sf.Alignment = StringAlignment.Center;
        sf.LineAlignment = StringAlignment.Center;
        sf.HotkeyPrefix = this.ShowKeyboardCues ? HotkeyPrefix.Show : HotkeyPrefix.Hide;

        if (!this.Enabled) {    //绘制禁用时候外观
            this.DrawDisable(g, sf);
            base.OnPaint(e);
            return; //如果中途要返回 记得调用base 不然用户绑定的Paint事件不会触发
        }
        if (m_bMouseDown) {         //如果可用状态根据情况 填充底色
            g.FillRectangle(Brushes.DarkSlateBlue, this.ClientRectangle);
        } else if (m_bMouseOn) {    //注意MouseDown应该优先于MouseOn判断
            g.FillRectangle(Brushes.SlateBlue, this.ClientRectangle);
        } else {
            g.FillRectangle(Brushes.Black, this.ClientRectangle);
        }
        if (this.ShowFocusCues && this.ContainsFocus) {     //绘制焦点框
            using (Pen p = new Pen(Color.Gray)) {
                p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
                g.DrawRectangle(p, 2, 2, this.Width - 5, this.Height - 5);
            }
        }
        g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, sf);
        base.OnPaint(e);
    }

    protected virtual void DrawDisable(Graphics g, StringFormat sf) {
        g.FillRectangle(Brushes.LightGray, this.ClientRectangle);
        g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, sf);
        g.TranslateTransform(-1, -1);
        g.DrawString(this.Text, this.Font, Brushes.Gray, this.ClientRectangle, sf);
        g.ResetTransform();
    }
}
代码非常简单 不用做过多解释也能明白是啥意思 为了减少代码量 所以控件里面的那些颜色我直接给死的 建议还是提供属性 让用户去设置 比如鼠标移动上去时候的背景颜色[MouseOnBackColor] 鼠标点击下去的背景颜色[MouseDownBackColor] 当然默认时候的颜色就可以不用提供属性了 直接用控件自己的[BackColor]属性就可以了

如果你有用到控件自己的[BackColor]的话不用写代码去绘制 因为Paint本身就是在控件上绘制的 而[BackColor][BackgroundImage]之类的属性控件是自身处理过了的 所以你没有什么特殊需求 完全没有必要自己再去处理一次 因为你怎么设置这个值 控件背景就是什么颜色 只是这里我强制的控件上填充了一个黑色区域

最后来一个效果图:

不过注意:这里只是为了演示案例 如果真打算从Button角度去做的话 还是建议不要直接继承Control 而继承Button去写 因为如果直接继承Control的话 一些Button具有的特性就没了 虽然平时使用的时候觉得Button就是拿来被人点的 没啥特殊的地方  比如有时候 要设置一个窗体的默认按键的时候(点击回车或者ESC的时候) 这个时候就需要一个Button控件了


添加时间:2014-03-31 22:38:53 编辑时间:2016-11-07 14:59:11 阅读:2548 
C#自定义控件开发 C#控件开发
还没有人留言 要不你来抢一个沙发?
  • 编写评论

      我觉得区分大小写是一个码农的基本素质
[访问统计] 今天:14 总数:262825 提示:未成年人 请在大人陪同下浏览本站内容 还有:世界上最帅的码农 -> 石头 RSS:http://st233.com/rss Powered by -> Crystal_lz