现在的位置: 首页 > 综合 > 正文

画图板程序

2012年08月21日 ⁄ 综合 ⁄ 共 8011字 ⁄ 字号 评论关闭

 

最近做一些图像处理相关的东西 还是有那么一点点心得分享下

第一部分:

做一个画图板程序类windows里的mspaint
首先是想到怎么把基本功能实现 铅笔阿 画线阿 画圆 画框阿 啥的,为了突出咱的不同 咱做一个对图形进行拖动 调整大小的功能 。
要对图形重定义 那么要对图像保存元数据以便以后调整 说得忽悠点就是序列化啥的 砖家们经常这样讲
画线 铅笔 画圆 画框 这些在.net的graphic里调用都一句话的事 然后在onpaint里进行重绘就可以了。
明白原理自己去实现也不是不可以 以前我写过一篇在winfrom里画直线的文章 只是我们没有必要再去造轮子。

我们先弄一个工具栏 整个工具切换效果 先拖个menustrip控件 然后加5个菜单项:


写menustrip的itemclicked事件的代码, 注意不是菜单项的click事件:

//工具切换效果
private void menuStrip1_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
{
    for (int i = 0; i < menuStrip1.Items.Count; i++)
    {
        menuStrip1.Items[i].BackColor = Color.FromName("Control");
    }
    e.ClickedItem.BackColor = Color.Green;
}

 然后再声明一个枚举类型 枚举5种工具,单击菜单项的时候进行切换:

private void 画线ToolStripMenuItem_Click(object sender, EventArgs e)
{
    type = toolType.line;
}

private void 铅笔ToolStripMenuItem_Click(object sender, EventArgs e)
{
    type = toolType.pencil;
}
toolType type;
enum toolType
{
    line, pencil,rec
}

 然后是画的过程,咱不能直接在窗体上画 咱得保存元数据阿,线段吗暂时用 Ilist<Point> 就可以 铅笔用graphicPath 矩形就用 Ilist<Rectangle>
画的时候根据鼠标 按下->拖动->释放 把图形添加到后台。onpaint事件的时候把图形重现。
下面是一些代码 我说过画线画框那些在.net里都是一句话调用的事,铅笔工具稍微复杂点 在最后面有说明:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    //for (int i = 0; i < line.Count; i += 1)
    //{
    //    if (i >= line.Count - 1)
    //        break;
    //    e.Graphics.DrawLine(Pens.Red, line[i], line[i + 1]);
    //}
    for (int i = 0; i < lines.Count; i += 2)
    {
        if (i >= lines.Count - 1)
            break;
        e.Graphics.DrawLine(Pens.Red, lines[i], lines[i + 1]);
    }
    foreach (Rectangle rec in recs)
    {
        e.Graphics.DrawRectangle(Pens.Green, rec);
    }
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    //line.Add(e.Location);
    Invalidate();
}

IList<Point> lines = new List<Point>();
IList<Rectangle> recs = new List<Rectangle>();
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
    startMouse = e.Location;
}
Point startMouse;

private void Form1_MouseUp(object sender, MouseEventArgs e)
{
    switch (type)
    {
        case toolType.line: {
            lines.Add(startMouse);
            lines.Add(e.Location);
            break;
        }
        case toolType.rec:
            {
                recs.Add(new Rectangle(startMouse, new Size
                    (e.Location.X - startMouse.X, e.Location.Y - startMouse.Y)));
                break;
            }
    }
    Invalidate();
}

 并且矩形这里也只对从左上起点到右下结束的这种情况作了处理 其他的情况没考虑,在完整代码里功能是实现了的。
还有就是怎么没有“即时效果” 就是画线断跟画框的拖拽效果,在onpaint最后加上这两句就可以了:

if (!mouseDown)
    return;
if (type == toolType.line)
    e.Graphics.DrawLine(Pens.Red, startMouse, tmpMouse);
if (type == toolType.rec)
    e.Graphics.DrawRectangle(Pens.Green, new Rectangle(startMouse, new Size
                    (tmpMouse.X - startMouse.X, tmpMouse.Y - startMouse.Y)));

 当然得加一个mouseDown的bool变量 让他在鼠标按下时为true 弹起时为false,再加一个tmpMouse的 Point变量  在鼠标移动时给他赋值。
以上所有源码下载 猛击此处
当当当当 现在一个画图板出来了吧基本上可以用了吧 挖哈哈 (¯▽¯;)


对了拖动功能呢?这个嘛其实很简单, 如果当前是对象选择工具(最后一个按钮) 在鼠标按下时把那个坐标拿到所有对象里去检测 如果有符合的 选定那个图形。
检测的方式嘛 自己去搞咯, 看某个点是否在一条线断上我那个 画直线 的文章里有 第二部分的源码里也有。检测矩形就更简单啦
鼠标移动的时候看当前是否检测并选择了对象 如果有 拖动就是啦,拖动的过程 这还不简单 更改当前对象的坐标就是啦 point.offset 根据当前鼠标位置改 不要告诉我你不会哈。
这些所有的在第二部分的源码里都有。

第二部分:

程序不是完了吗,为什么还要有第二部分,看完就知道了 并且后面的更精彩 嘿嘿,并且第二部分所有代码都是重新写的。
上面那样依然不是好的结构 为了更系统些包括以后功能的扩展 程序结构的清晰 ,我们得用面向对象的方式去设计它 把 数据模型 代码逻辑分离, 功能的分类 定义好
想象一下上中学几何课的时候在黑板上画图,比如这是一个系统。它有黑板(Board类)这个对象对不 它是所有图形的承载体 包括背景色 粉笔 当前正在画的图形 等,
图形是预定好的 有三角形 矩形 各种图形(Circle line pencil square)
但是他们都有相同的特性比如在黑板上的位置 线条的颜色 填充颜色 他们有共同的基类(Shape) 这点我们可以通过继承来实现:

来复习下最基本的继承以及virtual方法实现:

class Program
{
    static void Main(string[] args)
    {
        IList<father> fathers = new List< father>();
        fathers.Add(new son());
        fathers.Add(new son());
        fathers[0].method();
        fathers[0].method2();
    }
}

class father
{
    public virtual void method()
    {
        Console.WriteLine("father's method");
    }
    public virtual void method2()
    {
        Console.WriteLine("father's method2");
    }
}
class son : father {
    public override void method()
    {
        Console.WriteLine("son's method");
        //base.method();
    }
}

输出:
son's method
father's method2
说到底virtual跟override就是面向对象的精华所在, 子类实现了调用子类的 子类未实现调用父类的。 包括VC++的MFC到处都用到了这种模式。
原来从C转到C++的也根本没得这么多乱七八糟的面向对象设计的理论,只是有这么一个概念而已。

这是表现图形及方法实现部分代码,这么做的目的就是为了对数据模型进行定义 便于扩展,比如我以后没有画矩形的需求了 可以把矩形那个类删掉
以后想画五角星了 可以增加一个五角星的类 继承Shape。比如每种图形都有相同的背景色。可以在Shape 定义showBackground()供每个继承的对象调用。
而不用每个都重新实现这个方法。

#region 图形基类
public  class Shape
{
    public Rectangle region;//图形的大小及位置
    public int lineAlpha=100;//透明度
    public int layer=1;//层叠位置
    public Pen line = new Pen(Color.Red, 1);//线段属性
    public Color bgColor = Color.Lavender;//背景色
    public int bgColorAlpha = 50;
    public Point mouseStart = Point.Empty;//鼠标起点
    public Point mouseEnd = Point.Empty;//鼠标终点
    public bool selected = false;

    public Shape() { }


    public Shape(Color _lineColor, Point _mouseStart)
    {
        line.Color = _lineColor; this.region = new Rectangle(); region.Location = _mouseStart; region.Width = 0; region.Height = 0; 
    }
    //绘制(其实就是数据更新)
    public virtual void draw(Point p){}

    public virtual void move(int x, int y)//根据偏移量移动
    {
        region.Location.Offset(x, y);
    }

    public virtual bool select(Point p)
    {
        return false;
    }
            
    public virtual void show(Graphics handle)//显示
    {
        handle.DrawRectangle(line, this.region);
    }

}
#endregion

#region 线段
public class Line : Shape
{
    
    //通过两点初始化一条线段
    public Line(Point _start, Point _end)
    {
        this.mouseStart = _start; this.mouseEnd = _end;

        region.X = mouseEnd.X > mouseStart.X ? mouseStart.X : mouseStart.X - (mouseStart.X - mouseEnd.X);
        region.Y = mouseEnd.Y > mouseStart.Y ? mouseStart.Y : mouseStart.Y - (mouseStart.Y - mouseEnd.Y);
        region.Width = Math.Abs(mouseStart.X - mouseEnd.X);
        region.Height = Math.Abs(mouseStart.Y - mouseEnd.Y);
    }
    public Line() { }
    //通过一点初始化一条线段(起点跟终点同一个坐标不能称之为线段 准确的说应该是点)
    public Line(Point _start)
    {

        line.Color = Color.Red;
        this.mouseStart = _start; this.mouseEnd = Point.Empty;

        region.X = mouseStart.X;
        region.Y = mouseStart.Y;
        region.Width = 0;
        region.Height = 0;
    }
    //绘制(对两个点进行数据更新)
    public override void draw(Point p)
    {
        if (mouseStart == Point.Empty)
        { this.region = new Rectangle(); region.Location = p; region.Width = 0; region.Height = 0; }
        else //if (mouseEnd == Point.Empty)
        {
            mouseEnd = p;

            region.X = mouseEnd.X > mouseStart.X ? mouseStart.X : mouseStart.X - (mouseStart.X - mouseEnd.X);
            region.Y = mouseEnd.Y > mouseStart.Y ? mouseStart.Y : mouseStart.Y - (mouseStart.Y - mouseEnd.Y);
            region.Width = Math.Abs(mouseStart.X - mouseEnd.X);
            region.Height = Math.Abs(mouseStart.Y - mouseEnd.Y);
        }
    }

    //对当前实例进行显示(根据起始坐标画一条直线)
    public override void show(Graphics handle)
    {
        handle.DrawLine(line, mouseStart, mouseEnd);
    }
    //移动
    public override void move(int x, int y)
    {
        mouseStart.Offset(x, y);
        mouseEnd.Offset(x, y);

        region.Location.Offset(x, y);
    }
     
    public override bool select(Point mouse)
    {
        Util u = new Util();
        if (u.linedetector(mouse, mouseStart, mouseEnd))
        {
            selected = true;
            return true;
        }

        selected = false;
        return false;
    }
}
#endregion

 
跟计算机相关的东西 特别是程序开发,有些东西是要搞清楚原理的 原理弄懂了 什么都好办 ,不要只浮于表面的一些啥工具啊技术的 发现培训机构出来的都有点这种。

最菜的时候弄铅笔工具 我想用鼠标跟随的方式不断drawRectangle 这样就可以把鼠标笔迹画出来了,最后发现鼠标是跟不上的 画出来断断续续的,
我弄这个之前也没有看过其他人的代码,想想也确实应该这样实现啊。
原来的铅笔工具代码。。。

private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    Graphics g = Graphics.FromHwnd(this.Handle);
    g.DrawRectangle(Pens.Red, new Rectangle(e.Location, new Size(2, 2)));
}

 
这个看上去有点像橡皮擦哈
百度上搜的东西都是废的。始终找不到原因 到底是鼠标没有捕捉到那个点么,事实上鼠标的每一个移动的坐标系统底层都是会捕捉到的 只是画的速度跟不上,
鼠标移动慢点还好 你说移动快了刷刷的 不断的drawRectangle多费系统资源啊 怎么可能跟得上 可能它也是根据CPU时间片来的呗 所以有些就漏掉了呗。
可能有些原理性的东西想想大概也就明白了  但是你得去琢磨啊 否则做什么程序开发,现在做程序设计的早没有以前C语言时候那些人的认真跟谨慎了
一天费劲心思去研究那些数学算法 谭大师的《C程序设计》是本好书 有难度但是不是太难 只要认真看都还是看得懂的。
后来知道了有graphicsPath这个东西做什么用的听名字就知道啊 然后在鼠标移动的时候不断的graphicsPath.addLine 把鼠标移动的路径用graphicsPath连起来
果然一试就成功 就跟Windows里的画图板一模一样 我想他那里面也是采用这种方式吧。现在铅笔工具代码 注意没有用到graphicpath

private void Form1_Paint(object sender, PaintEventArgs e)
{
    for (int i = 0; i < line.Count; i += 1)
    {
        if (i >= line.Count - 1)
            break;
        e.Graphics.DrawLine(Pens.Red, line[i], line[i + 1]);
    }
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
    line.Add(e.Location);
    Invalidate();
}
IList<Point> line = new List<Point>();

 

如果循环里换成 i+=2 则变成虚线,并且值越大虚得越凶 有意思吧 (¯▽¯;)  。实际上多边形也可以用这种方式来做 数组里两个一组两个一组的point值 明白赛。

别忘了加上双缓冲代码

this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.
this.SetStyle(ControlStyles.DoubleBuffer, true);

对图像指定部分进行清除 达到橡皮擦的效果想起来好像很复杂 其实你反过来想不就得了吧,橡皮擦是一种画笔 一种特殊的画笔 背景色的画笔。

还后onPaint的概念自MFC里就有的并不是winfrom里才有 这是Windows窗体的一个重要概念 必须弄明白。
本来参数e里就有e.graphics  以前看到网上在鼠标移动时graphics g=graphics.fromhandle(this.handle)  就一直这样用 结果就是老是一闪一闪的,
这次在onpaint直接用e.graphics就不闪了。可见onpaint函数是直接接管底层的windwos重绘消息 有特别作用的 是不一样的。有.net了 有winform了 方便了
所有的都是基于托管平台编程了 我们不用去处理底层windows消息了 于是我们不明白原理了。

还有就是做这种绘图或者即时性效果的代码里边 效率很重要,因为要不断的重绘不断的重绘可能每秒钟那段代码都被运行无数次了。
代码需要优化 包括各种条件判断 这样可以提高效率 一条 确保在真正需要的时候代码才会被执行 学校里那些烂代码就不要整到里面了 都是从烂代码过来的呗。
程序的设计也是很重要的 本人从不在winform里整些乱七八糟的代码 winfrom里只负责调用  哪个类继承哪个类  程序结构整整洁洁的 多好。

又讲了这么多没用的

最后是画图板最终程序源码 猛击此处 怕有的朋友运行不起是在.net2.0模式下编译的,这里就是个学习的地方 别藏着掖着 整个东西出来还不给源码给别人研究 多烦啊
如果有用了咱的代码的请留个名儿哈。

程序还有诸多未完善的地方比如椭圆区域的判定 变形 等 望改进。椭圆的拖动功能暂未实现 其他的都OK。
还有拖动时图形抓取的容差是5个像素 针对线段  抓取拖动的过程没有任何颜色跟光标的变化 。同鞋们注意咯 免得说俺功能没实现哈。

其实画图板不难 真谛不难 就算你要做成矢量的 可进行后处理的 可拖动的 变形的 多图层的 就算做到photoshop那样 也不是什么很困难的事 大家可以看到在《图片裁剪效果》的文章里alpha透明效果圆角矩形等等我都已经实现了。这个实际上不算图像处理哈 ,要做专门的图象处理需要有专门的理论 算法  高数 平面几何等等 本人承认在这方面还欠缺 还需努力学习 加油。

又是熬夜写这个鬼东东 靠 五点多了。

抱歉!评论已关闭.