IT/C#

[C#] 윈도우 그림판 기능 구현해보기 (PictureBox 그리기 기능, Undo/Redo 기능, 단축키 사용 방법)

Ella.J 2020. 10. 16. 21:13
728x90
반응형

그림판에 여러 기능들 중에서 크게 가지 기능을 구현해보겠습니다.

1. 도형(사각형, 원형, 선형직선) 그리기 기능 2. 실행 취소(Undo)/다시 실행(Redo) 기능 가지 입니다.

그리고 흔히들 사용하는 Ctrl + Z / Ctrl + Shift + Z 단축키 기능 추가해보겠습니다.


1. 메인화면 구성

먼저, 메인화면 구성입니다. Winform 기본 도구상자에서 PictureBox, MenuStirp 가져와서 다음과 같이 만듭니다.

각각의 이름은 menuStrip에서 입력한 메뉴 이름이 앞에 자동으로 붙게 되므로 변경하지 않으셔도 됩니다.

menu 구성은 아래 사진과 같이 적용해주면 됩니다.

 

UI 간단하게 위와 같이 만들고, 코드 작성으로 넘어가보겠습니다.


2. 코드 작성 - 전역 변수 선언

1) 이미지 그리기에 필요한 변수 선언

1
2
3
4
5
private static Point clickPoint;
private static Point UpPoint;
private static Bitmap OriginalBmp;
private static Bitmap DrawBmp;
private static Rectangle imgRect;
cs

2) 그림판 enum 변수 선언

필요한 기능이 있으면 여기서 확장해서 사용 가능합니다. ex) select, crop 등등

enum을 잘 활용하면 코드가 깔끔해지고 유지보수가 편리해집니다. 굿굿 : )

1
2
3
4
5
6
7
8
public static PaintTools toolType { get; set; }
public enum PaintTools
{
    IDLE = default,
    DrawLine,
    DrawRectangle,
    DrawCircle
}
cs

3) 그림위치그림 위치, 도형형태 저장할 List

1
2
3
4
public List<Rectangle> listRect = new List<Rectangle>();
public List<Rectangle> tempRect = new List<Rectangle>();
public List<PaintTools> listTool = new List<PaintTools>();
public List<PaintTools> tempTool = new List<PaintTools>();
cs

3. 코드 작성 - 초기 설정 및 그림판 도구 지정

1) 프로그램 시작 기본 배경화면 지정

배경 이미지(OriginalBmp) 위에 그림을 그리고 그림을 그릴 때마다 새로운 Bitmap 이미지(DrawBmp) 저장하기 위해서 먼저 배경 이미지를 로드해서 OriginalBmp 저장해줍니다.

Application.StartupPath : 응용 프로그램을 시작한 실행 파일의 경로로써 보통 Debug 폴더가 됩니다.

Debug 폴더 안에 DefaultBackground.png 파일을 복사 붙여넣기 해주어야 정상적으로 로드할 있습니다.

DefaultBackground.png
0.01MB

1
2
3
4
5
6
7
8
public Form1()
{
    InitializeComponent();
    //White BackgroundImage Load
    pictureBox1.Image = new Bitmap(Application.StartupPath + @"\DefaultBackground.png");
    OriginalBmp = (Bitmap)pictureBox1.Image;
    imgRect = new Rectangle(00, pictureBox1.Width, pictureBox1.Height);
}
cs

2) 그림판 선택시 tooType 지정해주기

menuStrip에서 각각의 메뉴를 클릭했을 toolType 지정해 줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void rectangleToolStripMenuItem_Click(object sender, EventArgs e)
{
    toolType = PaintTools.DrawRectangle;
}
 
private void circleToolStripMenuItem_Click(object sender, EventArgs e)
{
    toolType = PaintTools.DrawCircle;
}
 
private void lineToolStripMenuItem_Click(object sender, EventArgs e)
{
    toolType = PaintTools.DrawLine;
}
cs

4. 코드 작성 - 도형(사각형, 원형, 선형) 그리기

다음으론 각각의 도형그리기 기능입니다.

마우스 왼쪽 클릭 후에 마우스를 드래그하면서 도형이 그려지는 것을 구현해보겠습니다.

 

1) 먼저 마우스 처음 클릭 위치를 clickPoint 저장한 후에

1
2
3
4
5
6
7
8
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        //마우스 클릭 위치 저장
        clickPoint = new Point(e.X, e.Y);
    }
}
cs

2) 마우스 이동마다 그림을 새로 그려줍니다.

그림판처럼컬러, 굵기를 선택하여 변경해줄 수도 있습니다.

현재 작성된 아래 코드는 검은색의 굵기 3 선을 그리고 있으나,

Pen pn = new Pen(Color.Red);

Pn.Width = 10;

위와 같이 변경해서 사용할 수도 있습니다.

아래의 코드는 각각의 선택된 그림판 툴에 따라 그림을 그립니다.

사각형 => g.DrawRectangle(pen, x, y, width, height)

원형 => g. DrawEllipse(pen, x, y, width, height)

선형 => g.DrawLine(pen, point1.x, point1.y, poin2.x, point2.y)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        float w = Math.Abs(clickPoint.X - e.X);
        float h = Math.Abs(clickPoint.Y - e.Y);
 
        Pen pn = new Pen(Color.Black);
        pn.Width = 3;
        Graphics g = pictureBox1.CreateGraphics();
        pictureBox1.Refresh();
 
        if (toolType == PaintTools.DrawRectangle) //사각형 그리기
        {
            if (e.X > clickPoint.X)
            {
                if (e.Y > clickPoint.Y) g.DrawRectangle(pn, clickPoint.X, clickPoint.Y, w, h);
                else g.DrawRectangle(pn, clickPoint.X, e.Y, w, h);
            }
            else
            {
                if (e.Y > clickPoint.Y) g.DrawRectangle(pn, e.X, clickPoint.Y, w, h);
                else g.DrawRectangle(pn, e.X, e.Y, w, h);
            }
        }
        else if (toolType == PaintTools.DrawCircle) //원형 그리기
        {
            if (e.X > clickPoint.X)
            {
                if (e.Y > clickPoint.Y) g.DrawEllipse(pn, clickPoint.X, clickPoint.Y, w, h);
                else g.DrawEllipse(pn, clickPoint.X, e.Y, w, h);
            }
            else
            {
                if (e.Y > clickPoint.Y) g.DrawEllipse(pn, e.X, clickPoint.Y, w, h);
                else g.DrawEllipse(pn, e.X, e.Y, w, h);
            }
        }
        else if (toolType == PaintTools.DrawLine) //선형 그리기
        {
            g.DrawLine(pn, clickPoint.X, clickPoint.Y, e.X, e.Y);
        }
    }
}
cs

3) 최종적으로 마우스 왼쪽버튼을 뗐을 배경이미지에 그림을 그리고 리스트에 그린 정보를 저장한 DrawBmp으로 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        UpPoint.X = e.X;
        UpPoint.Y = e.Y;
 
        float w = Math.Abs(clickPoint.X - e.X);
        float h = Math.Abs(clickPoint.Y - e.Y);
 
        Pen pn = new Pen(Color.Black);
        pn.Width = 3;
        Rectangle rect = new Rectangle();
        Graphics g = pictureBox1.CreateGraphics();
 
        if (toolType == PaintTools.DrawRectangle) //사각형 그리기
        {
            if (e.X > clickPoint.X)
            {
                if (e.Y > clickPoint.Y) rect = new Rectangle(clickPoint.X, clickPoint.Y, (int)w, (int)h);
                else rect = new Rectangle(clickPoint.X, e.Y, (int)w, (int)h);
            }
            else
            {
                if (e.Y > clickPoint.Y) rect = new Rectangle(e.X, clickPoint.Y, (int)w, (int)h);
                else rect = new Rectangle(e.X, e.Y, (int)w, (int)h);
            }
        }
        else if (toolType == PaintTools.DrawCircle) //원형 그리기
        {
            if (e.X > clickPoint.X)
            {
                if (e.Y > clickPoint.Y) rect = new Rectangle(clickPoint.X, clickPoint.Y, (int)w, (int)h);
                else rect = new Rectangle(clickPoint.X, e.Y, (int)w, (int)h);
            }
            else
            {
                if (e.Y > clickPoint.Y) rect = new Rectangle(e.X, clickPoint.Y, (int)w, (int)h);
                else rect = new Rectangle(e.X, e.Y, (int)w, (int)h);
            }
        }
        else if (toolType == PaintTools.DrawLine) //선형 그리기
        {
            rect = new Rectangle(clickPoint.X, clickPoint.Y, clickPoint.X + e.X, clickPoint.Y + e.Y);
        }
 
        //리스트에 Rectangle 정보, Tool Type 정보 저장하기
        listRect.Add(rect);
        listTool.Add(toolType);
        DrawBitmap();
    }
}
 
private void DrawBitmap()
{
    if(OriginalBmp != null)
    {
        DrawBmp = (Bitmap)OriginalBmp.Clone();
        for (int i = 0; i < listRect.Count; i++)
        {
            double wRatio = (double)OriginalBmp.Width / pictureBox1.Width;
            double hRatio = (double)OriginalBmp.Height / pictureBox1.Height;
            Rectangle rect = new Rectangle((int)(listRect[i].X * wRatio), (int)((listRect[i].Y) * hRatio),
                    (int)(listRect[i].Width * wRatio), (int)(listRect[i].Height * hRatio));
            Pen pn = new Pen(Color.Black);
            pn.Width = (float)(3 * wRatio);
 
            using (Graphics g = Graphics.FromImage(DrawBmp))
            {
                if (listTool[i] == PaintTools.DrawRectangle) g.DrawRectangle(pn, rect);
                else if (listTool[i] == PaintTools.DrawCircle) g.DrawEllipse(pn, rect);
                else if (listTool[i] == PaintTools.DrawLine) g.DrawLine(pn, new Point(rect.X, rect.Y), new Point(rect.Width - rect.X, rect.Height - rect.Y));
            }
        }
        pictureBox1.Image = DrawBmp;
    }
}
 
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    if (OriginalBmp != null)
    {
        e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        if (listRect.Count > 0 && DrawBmp != null)
        {
            e.Graphics.DrawImage(DrawBmp, imgRect);
        }
        else
        {
            e.Graphics.DrawImage(OriginalBmp, imgRect);
        }
    }
}
 
cs

5. 코드 작성 - 실행 취소(Undo), 다시 실행(Redo) 기능 만들기

다음으로는 실행취소(Undo), 다시실행(Redo) 기능인데, 아래의 코드로 보겠습니다.

1) 각각의 undo, redo 버튼클릭 이벤트에 다음과 같이 코딩해줍니다.

현재 그려진 도형 정보는 listRect 리스트, 실행 취소 상태인 도형 정보는 tempRect 리스트 저장하여 실행취소실행 취소, 다시실행 할때마다 리스트에 넣고 빼는 방식으로 사용해주었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void undoToolStripMenuItem_Click(object sender, EventArgs e)
{
    if(listRect.Count > 0)
    {
        //Undo 실행 취소
        //맨 마지막 list에 있던 거 temp에 저장하고 마지막 list 삭제
        tempRect.Add(listRect[listRect.Count - 1]);
        listRect.RemoveAt(listRect.Count - 1);
        tempTool.Add(listTool[listTool.Count - 1]);
        listTool.RemoveAt(listTool.Count - 1);
        pictureBox1.Refresh();
        DrawBitmap();
    }
}
 
private void redoToolStripMenuItem_Click(object sender, EventArgs e)
{
    if(tempRect.Count > 0)
    {
        //Redo 다시 실행
        //맨 마지막 temp에 있던 거 list에 저장하고 마지막 temp 삭제
        listRect.Add(tempRect[tempRect.Count - 1]);
        tempRect.RemoveAt(tempRect.Count - 1);
        listTool.Add(tempTool[tempTool.Count - 1]);
        tempTool.RemoveAt(tempTool.Count - 1);
        pictureBox1.Refresh();
        DrawBitmap();
    }
}
cs

 

2) 이를 간단하게 단축키로도 사용하기 위해서 Form1 속성창에서 KeyDown 이벤트를 등록하고 아래와 같이 코딩해줍니다.

Ctrl + Z 단축키 입력 : e.Control && !e.Shift && e.KeyCode == Keys.Z

Contrl 클릭상태이나 Shift 클릭한 상태가 되면 안되기 때문에 Shift 클릭 상태가 아닌 것을 확인해 주어야 합니다.

Ctrl + Shift + Z 단축키 입력 : e.Control && e.Shift && e.KeyCode == Keys.Z

If else 안의 내용은 위의 버튼 클릭 이벤트의 내용과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    //Undo & Redo Shortcut 실행 취소 & 다시 실행 단축키
    if (e.Control && !e.Shift && e.KeyCode == Keys.Z)
    {
        //CTRL + Z : Undo 실행 취소
        if (listRect.Count > 0)
        {
            //맨 마지막 list에 있던 거 temp에 저장하고 마지막 list 삭제
            tempRect.Add(listRect[listRect.Count - 1]);
            listRect.RemoveAt(listRect.Count - 1);
            tempTool.Add(listTool[listTool.Count - 1]);
            listTool.RemoveAt(listTool.Count - 1);
            pictureBox1.Refresh();
            DrawBitmap();
        }
    }
    else if (e.Control && e.Shift && e.KeyCode == Keys.Z)
    {
        //CTRL + SHIFT + Z : Redo 다시 실행
        if (tempRect.Count > 0)
        {
            //맨 마지막 temp에 있던 거 list에 저장하고 마지막 temp 삭제
            listRect.Add(tempRect[tempRect.Count - 1]);
            tempRect.RemoveAt(tempRect.Count - 1);
            listTool.Add(tempTool[tempTool.Count - 1]);
            tempTool.RemoveAt(tempTool.Count - 1);
            pictureBox1.Refresh();
            DrawBitmap();
        }
    }
}
 
cs

6. 실행 동영상 보기

그럼 포스팅은 여기까지 마치고 궁금하신 내용이나 잘못된 부분이 있다면 댓글로 남겨주시고,

포스팅이 마음에 드셨다면 공감 꾹! 광고 꾹! 눌러주시면 큰 힘이 됩니다♥

 

다음 포스팅 보러가기

2021.03.31 - [IT/C#] - [C#] 윈도우 그림판 기능 추가해보기 (PPT처럼 도형과 도형을 직선으로 잇기)

 

[C#] 윈도우 그림판 기능 추가해보기 (PPT처럼 도형과 도형을 직선으로 잇기)

어떤 분이 댓글로 도형과 도형을 클릭하면 직선으로 선이 이어지게 되는 것을 어떻게 구현하면 될지 질문해 주셨다. 그래서 도전해봤다. 컨셉으로 생각한 것은 다음과 같다. PPT처럼 도형 위에

ella-devblog.tistory.com

2023.04.07 - [IT/C#] - [C#] 윈도우 그림판 만들기 3탄(?) 테이블 그리기 (PictureBox Draw Table with Col, Row)

 

[C#] 윈도우 그림판 만들기 3탄(?) 테이블 그리기 (PictureBox Draw Table with Col, Row)

2020.10.16 - [IT/C#] - [C#] 윈도우 그림판 기능 구현해보기 (PictureBox 그리기 기능, Undo/Redo 기능, 단축키 사용 방법) [C#] 윈도우 그림판 기능 구현해보기 (PictureBox 그리기 기능, Undo/Redo 기능, 단축키 사용

ella-devblog.tistory.com

 

728x90
반응형