C# 프로그래밍 (3)
포스트
취소

C# 프로그래밍 (3)

Collection

1
2
3
4
5
6
7
8
9
10
11
12
13
class Program {
    public static void Main() {
        int[] arr = { 1, 2, 3, 4, 5 };
        List<int> s = new List<int>();

        s.Add(1);
        s.Add(2);
        s.Add(3);

        s[0] = 10;
        s[1] = 30;
    }
}
  • Collection : 여러개의 데이터를 저장하는 자료구조
  • Array: 크기가 고정된 Collection
    • 불편하지만 약간 빠름
  • List: 크기가 가변적인 Collection
    • 편리하지만 약간 느림
    • 파이썬 list와 거의 같음
  • 인덱스 접근은 다 똑같이 대괄호 사용
1
2
3
4
5
6
7
8
9
10
11
using System.Drawing;

class Program {
    public static void Main() {
        List<int> s1 = new List<int>(); // 정확한 표기법
        List<int> s2 = new(); // 단축 표기법

        Point pt1 = new Point(1, 1);
        Point pt2 = new (1, 1); // 위와 동일
    }
}
  • Collection 단축 표기법
  • 컬렉션 생성 시 타입 생략 가능: 컬렙션<타입> 변수명 = new();
  • 컴파일러가 타입을 추론하여 자동으로 지정

메소드 오버라이드

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
using static System.Console;

class Animal {
    public void Cry1() { WriteLine("1. Animal Cry"); }
    public virtual void Cry2() { WriteLine("1. Animal Cry2"); }
}
class Dog : Animal {
    public new void Cry1() { WriteLine("2. Dog Cry1"); }
    public override void Cry2() { WriteLine("2. Dog Cry2"); }
}

class Program {
    public static void Main() {
        Animal a = new Animal();
        Dog d = new Dog();

        a.Cry1();  // 1. Animal Cry1
        d.Cry1();  // 2. Dog Cry1

        Animal ad = d;

        ad.Cry1();  // 1. Animal Cry1 : static binding이므로 컴파일러가 ad의 타입인 Animal로 판단하여 Animal의 Cry1() 호출
        ad.Cry2();  // 2. Dog Cry2 : dynamic binding이므로 실제 ad가 가리키는 객체인 Dog의 Cry2() 호출
    }
}
  • 메소드 오버라이드 : 기반 클래스의 메소드를 파생 클래스에서 다시 만드는 것. 파생 클래스에서는 new를 표기하여 오버라이드임을 표시해야 함.
  • Function(Method) Binding
    1. static binding : 컴파일 시간에 컴파일러가 어떤 함수를 호출할지 결정.
      • 컴파일시간에는 대상체의 타입은 알수 없고, 컴파일러가 알수 있는 것은 해당 객체 자체의 타입만. 따라서 컴파일러가 결정하면 실제 객체 타입이 아닌 Reference 타입으로 호출됨.
      • 빠르고 비논리적
      • C++/C#의 기본값
    2. dynamic binding : 컴파일 시 객체가 가리키는 메모리를 조사하는 기계어(IL) 코드를 생성해 놓고, 실행 시 실제 메모리를 조사해서 무엇을 호출할지 결정.
      • 느리고 논리적
      • Java, Python, Kotlin, Swift 등 대부분의 객체지향 언어의 기본 바인딩
      • C++/C#의 virtual method
      • 기반 클래스에서 정의할 때는 virtual 키워드 사용, 파생 클래스에서 재정의할 때는 override 키워드 사용

객체 지향 프로그래밍

코드 보기
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
using static System.Console;

// 2. 모든 도형의 공통의 기반 클래스인 Shape 도입
// 9. abstract method가 하나라도 있다면 클래스도 abstract가 되어야 한다.
abstract class Shape {
    private int color = 0;
    public int Color { get { return color; } set { color = value; } }
    // 8. 재정의할 필요가 없으면 virtual하지 말자
    public void SetColor(int c) {  color = c; }
    // 8. 재정의할 필요가 없으면 virtual하지 말자 (이건 재정의할 필요가 있음)
    public virtual int GetArea() { return -1; }
    // 5. 모든 도형의 공통의 특징은 반드시 기반 클래스에도 있어야 한다.
    // 6. 기반 클래스 메소드중 파생 클래스가 다시 만들게 되는 것은 대부분 virtual method 로 해야 한다.
    public abstract void Draw();
    // 7. 자신의 복사본을 만드는 가상 메소드는 널리 알려진 디자인 기술
    public abstract Shape Clone();
}

// 1. 모든 타입을 클래스로 만든 후 사용하면 편리하다.
class Rect : Shape {
    public override void Draw() { WriteLine("Draw Rect"); }
    public override Shape Clone() {
        Rect r = new Rect();
        r.Color = this.Color;
        return r;
    }
}

class Circle : Shape {
    public override void Draw() { WriteLine("Draw Circle"); }
    public override Shape Clone() {
        Circle c = new Circle();
        c.Color = this.Color;
        return c;
    }
}
class Triangle : Shape {
    public override void Draw() { WriteLine("Draw Triangle"); }
    public override Shape Clone() {
        Triangle t = new Triangle();
        t.Color = this.Color;
        return t;
    }
}

class Program {
    public static void Main() {
        List<Shape> s = new List<Shape>();
        while (true) {
            int cmd = int.Parse(ReadLine());

            if (cmd == 1) {
                s.Add(new Rect());
            }
            else if (cmd == 2) {
                s.Add(new Circle());
            }
            else if (cmd == 3) {
                s.Add(new Triangle());
            }
            else if (cmd == 8) {
                Write("복제할 도형의 인덱스 >> ");
                int k = int.Parse(ReadLine());
                s.Add(s[k].Clone());
            }
            else if (cmd == 9) {
                foreach (var e in s) {
                    e.Draw();
                }
            }
        }
    }
}
  • 객체지향에 기반하여 그림 그리기 프로그램을 만들어봐라
  1. 모든 타입을 클래스로 만든 후 사용하면 편리하다.
  2. 모든 도형의 공통의 기반 클래스인 Shape 도입
    • 모든 도형은 공통의 특징이 있다 (예: color)
    • 공통의 기반 클래스인 Shape 가 있다면
      • 공통의 특징을 하나의 클래스(Shape) 에서 관리할수 있고
      • 모든 도형을 한개의 컬렉션(List) 에 보관할수 있다.
  3. 공통 기반 클래스로 컬렉션을 만들어도 각각의 고유 멤버에는 접근할 수 있어야 한다.
    • List에는 Rect, Circle 모두 보관할수 있지만 거기에 보관된 Rect, Circle 객체의 고유 멤버인 Draw() 메소드에는 접근할 수 없다.
  4. 캐스팅을 할 경우: 타입 확인 후 캐스팅이 가능하긴 하지만 코드가 너무 보기 않좋고 유지보수성이 나쁨.
    • OCP 개념: 기능 확장에 열려 있고(Open, 새로운 요소(클래스)가 추가되어도), 코드 수정에는 닫혀 있게(Close, 기존 코드는 수정되지 않게) 만들어야 한다는 이론(Principle)
  5. 모든 도형의 공통의 특징은 반드시 기반 클래스에도 있어야 한다.
    • 디자인 상의 규칙임
    • 모든 파생 클래스가 동일한 멤버를 갖는다면 기반 클래스도 그것을 갖고 있어야 기반 클래스에서도 참조 호출이 가능하다.
  6. 기반 클래스 메소드중 파생 클래스가 다시 만들게 되는 것은 대부분 virtual method 로 해야 한다.
    • 그래야 기반 클래스 참조로 호출시 파생 클래스에서 다시 만든 메소드 호출됨
    • 다형성(Polymorphism): 하나의 참조로 여러 형태의 객체를 참조할 수 있는 능력
      • 동일한 한 줄의 코드가 상황에 따라 다르게 동작하는 것.
      • 객체지향의 3대 특징 중 하나
        1. 캡슐화(Encapsulation): 필드를 private 보호해서 안전성 증가
        2. 상속(Inheritance): 공통의 특징을 기반클래스로 관리해서 코드 재사용성 증가
        3. 다형성(Polymorphism): 하나의 참조로 여러 형태의 객체를 참조할 수 있는 능력
  7. 자신의 복사본을 만드는 가상 메소드는 널리 알려진 디자인 기술
    • 객체의 복사본을 만드는 메소드인 Clone()을 기반 클래스에 virtual method로 만든다.
  8. 재정의할 필요가 없으면 virtual하지 말자
    • 파생 클래스가 다시 만들어야 하는 메소드 → virtual
    • 파생 클래스가 다시 만들필요 없는 메소드 → non-virtual (이 경우 virtual은 오버헤드가 있음)
  9. abstract method가 하나라도 있다면 클래스도 abstract가 되어야 한다.
    • 추상 클래스는 객체 생성이 안되고, 참조 타입으로만 사용 가능. (예: Shape s = new Rect(); // ok)
    • 추상(abstract) 메소드: 기반 클래스가 구현없이 이름만 약속하는 것, 파생 클래스는 반드시 구현해야 한다
    • 가상(virtual) 메소드: 기반 클래스가 기본 구현 제공, 파생 클래스는 그대로 쓰거나 다시 만들 수 있음. 다시 만드는 것을 선택사항으로 두고자 할 때 사용할 수 있음.
코드 보기
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
using System.Windows;

class MainWindow : Window {
    // 생성자 : 객체가 생성될 때 자동으로 호출되는 함수
    public MainWindow() {
        // 아래의 모든 속성은 기반 클래스인 Window로부터 상속받은 것
        Title = "Hello";
        Width = 500;
        Height = 500;
        Content = "ABCD";

        Topmost = true;  // 항상 다른 윈도우 위에 나와야만 할 것

        StackPanel sp = new StackPanel();
        Content = sp;

        Button b1 = new Button { Content = "버튼1", FontSize = 32 };

        sp.Children.Add(b1);

        Slider slider = new Slider();
        sp.Children.Add(slider);

        RadioButton radio = new RadioButton();
        sp.Children.Add(radio);

        sp.Children.Add( new Button { Content = "버튼2" } );
    }
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
        MessageBox.Show("LBUTTONDOWN");
        base.OnMouseLeftButtonDown(e);
        // base: 기반 클래스를 의미
        // 기반 클래스의 함수도 실행되도록 하기 위해 자동 생성된 코드
    }

    protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) {
        MessageBox.Show("RBUTTONDOWN");
        base.OnMouseRightButtonDown(e);
    }

    protected override void OnKeyDown(KeyEventArgs e) {
        MessageBox.Show("KEYDOWN");
        base.OnKeyDown(e);
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • WPF 일반적인 코딩 관례
    • Window 클래스를 직접 사용하지 않고 파생 클래스를 만들어 사용
  • 아래 메소드는 Window 클래스에 정의됨
    • virtual void OnMouseLeftButtuonDown(…) { }
    • virtual void OnMouseRightButtuonDown(…) { }
  • Content는 윈도우의 내용을 나타내지만, 단 하나만 설정할 수 있기 때문에 여러 개를 넣으려면 패널에 조립해서 그 패널을 넣어야 함

WPF 패널

코드 보기
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
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    public MainWindow() {
        StackPanel sp = new StackPanel();
        Content = sp;

        // sp.Orientation = Orientation.Horizontal;
        sp.Orientation = Orientation.Vertical; // 디폴트 값

        // 이제 패턴에 자식 컨트롤 부착
        sp.Children.Add(new Button() );
        sp.Children.Add(new Slider() );
        sp.Children.Add(new ProgressBar { Value=50, Height=20 } );
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • WPF에서 상속 구현을 해보자
  • 프레임워크는 사용 방법이 정해져 있다
    • WPF: 윈도우에 자식컨트롤을 여러 개 붙이려면 Panel을 사용해야 한다.
코드 보기
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
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    public MainWindow() {
        DockPanel dp = new DockPanel();
        Content = dp;

        // dockpanel 은 top, bottom, left, right 로 부착
        Button b1 = new Button { Content = "버튼1" };
        Button b2 = new Button { Content = "버튼2" };
        Button b3 = new Button { Content = "버튼3" };
        Button b4 = new Button { Content = "버튼4" };

        // 각 버튼이 DockPanel 에 어디에 놓일지 설정
        DockPanel.SetDock(b1, Dock.Top);
        DockPanel.SetDock(b2, Dock.Left);
        DockPanel.SetDock(b3, Dock.Right);
        DockPanel.SetDock(b4, Dock.Bottom);

        // 그리고, 각 버튼을 DockPanel 에 부착
        dp.Children.Add(b1);
        dp.Children.Add(b2);
        dp.Children.Add(b3);
        dp.Children.Add(b4);
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • DockPanel : 자식 요소들을 상하좌우로 배치하는 패널
    • DockPanel.SetDock(자식요소, Dock.Top/Bottom/Left/Right)
코드 보기
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
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    public MainWindow() {
        DockPanel dp = new DockPanel();
        Content = dp;

        TextBox tb = new TextBox();
        StackPanel sp = new StackPanel();

        DockPanel.SetDock(tb, Dock.Top);
        DockPanel.SetDock(sp, Dock.Bottom);

        dp.Children.Add(sp);
        dp.Children.Add(tb);

        sp.Orientation = Orientation.Horizontal;

        sp.HorizontalAlignment = HorizontalAlignment.Right;

        sp.Children.Add(new Button { Content = "OK", Width= 100, Height = 50 });
        sp.Children.Add(new Button { Content = "Cancel", Width = 100, Height = 50 });
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • StackPanel
    • StackPanel : 자식 요소들을 수평 또는 수직으로 배치하는 패널
    • StackPanel.Orientation = Orientation.Horizontal/Vertical
코드 보기
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
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    public MainWindow() {
        Grid grid = new Grid();
        Content = grid;

        // #1. Row, Col 값 설정
        // grid.SetRowCol(2, 3); // 일반적인 라이브러리는 이렇게 씀: 2개 컬럼, 3개 Row

        // WPF 는 독특한 방법 사용
        grid.RowDefinitions.Add(new RowDefinition());
        grid.RowDefinitions.Add(new RowDefinition());

        grid.ColumnDefinitions.Add(new ColumnDefinition());
        grid.ColumnDefinitions.Add(new ColumnDefinition());

        // #2. 컨트롤을 만들고
        Button b1 = new Button { Content = "버튼1" };
        Button b2 = new Button { Content = "버튼2" };

        // #3. 각 컨트롤이 Grid 에 어디에 놓일지 설정(아직 부착은 아님)
        // => 이 부분이 WPF 라이브러리의 독특한 코드
        Grid.SetRow(b1, 0);
        Grid.SetColumn(b1, 0);

        Grid.SetRow(b2, 1);
        Grid.SetColumn(b2, 1);

        // #4. 마지막으로 컨트롤을 Grid 에 부착
        grid.Children.Add(b1);
        grid.Children.Add(b2);
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • Grid Panel
    • Grid Panel : 엑셀처럼 격자형태로 관리하는 Panel
      1. [Row/Column]Definitions 속성에 [Row/Column]Definition 객체를 추가해서 Row/Column 수를 설정
      2. Grid.SetRow(컨트롤, RowIndex), Grid.SetColumn(컨트롤, ColumnIndex)로 각 컨트롤이 Grid의 어느 위치에 놓일지 설정
      3. Grid.Children.Add(컨트롤)로 컨트롤을 Grid에 부착 → 여기까지 해야 화면에 나온다

WPF로 퍼즐 게임 만들기

코드 보기
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
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Printing;

class MainWindow : Window {
    // 한개 블럭의 너비, 높이
    private int bw = 0; // block width
    private int bh = 0; // block height

    // 블럭의 갯수, 5 가 아닌 다른 것으로 하려면 COUNT 초기값 변경
    private const int COUNT = 5; // 상수
    private Grid grid = new();
    private void InitPanel() {
        // #1. Grid 생성
        grid = new Grid();

        // #2. Row, Col 설정 ( COUNT * COUNT)
        for (int i = 0; i < COUNT; i++) {
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
        }

        // #3. MainWindow 의 Content 로 grid 연결
        Content = grid;
    }
    private void MakeGridImage() {
        InitPanel();
        // #1. 자원의 경로를 관리하는 객체 생성
        Uri uri = new Uri("D3_ex/totoro.jpg", UriKind.Relative); // 그림 경로

        // #2. 그림(Bitmap) 을 Load 하는 객체 생성
        BitmapImage bm = new BitmapImage(uri);

        // Load된 비트맵에서 일부분을 Crop
        bw = (int)(bm.Width / COUNT);
        bh = (int)(bm.Height / COUNT);
        Int32Rect rc = new Int32Rect(0, 0, bw, bh);
        CroppedBitmap cb = new CroppedBitmap(bm, rc);

        // #3. Load 된 그림을 Content 로 연결 또는 panel 의 자식으로 넣으려면 Image 타입의 객체가 필요
        Image img = new Image();
        // img.Source = bm; // 전체 그림과 연결
        img.Source = cb; // 일부와 연결

        // 그리드 없이 크롭만 보여주려면 Content 에 img 객체 연결
        // Content = img;

        // 생성된 Image 객체를 Grid 0, 0 에 넣기
        Grid.SetRow(img, 0);
        Grid.SetColumn(img, 0);

        grid.Children.Add(img);
    }
    public MainWindow() {
        InitPanel();
        MakeGridImage();
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • WPF로 퍼즐 게임 만들기
    1. 이미지 하나 골라서
    2. 원하는 비율로 크롭하고
    3. 화면에 그리드 올리고
    4. 크롭 이미지 띄우기
    5. 4번까지의 과정을 생성자에서 별도의 함수로 분리하기
  • Uniform Resource Identity: URI
    • 자원의 위치를 나타내는 문자열
    • 형식: scheme:[//authority]path[?query][#fragment]
    • 예시: http://example.com/index.html, file:///C:/path/to/file.txt
    • WPF에서 이미지 로드 시 사용
      • 절대 경로: new Uri("C:\\\\path\\\\to\\\\image.jpg")
      • 상대 경로: new Uri("images/image.jpg", UriKind.Relative)
코드 보기
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
using System.IO;
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

class MainWindow : Window {
    private int bw = 0;
    private int bh = 0;
    private const int COUNT = 5;
    private const int EMPTY = COUNT * COUNT - 1;  // 여기보세요
    private Grid grid = new();
    public void InitPanel() {
        grid = new Grid();
        for (int i = 0; i < COUNT; i++) {
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
        }
        Content = grid;
    }

    public void MakeGridImage() {
        Uri uri = new Uri("D3_ex/totoro.jpg", UriKind.Relative);
        BitmapImage bm = new BitmapImage(uri);

        bw = (int)((int)bm.PixelWidth / COUNT);
        bh = (int)((int)bm.PixelHeight / COUNT);

        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                Int32Rect rc = new Int32Rect(x * bw, y * bh, bw, bh);  // 여기보세요
                CroppedBitmap cb = new CroppedBitmap(bm, rc);

                Image img = new Image();
                img.Source = cb;
                img.Stretch = Stretch.Fill;
                img.Margin = new Thickness(0.5);

                Grid.SetRow(img, y);  // 여기보세요
                Grid.SetColumn(img, x);

                grid.Children.Add(img);
            }
        }
    }

    public MainWindow() {
        InitPanel();
        MakeGridImage();
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • 조각낸 모든 이미지를 그리드에 배치하기
코드 보기
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
using System.IO;
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

class MainWindow : Window {
    private int bw = 0;
    private int bh = 0;
    private const int COUNT = 5;
    private const int EMPTY = COUNT * COUNT - 1;
    private Grid grid = new();
    private int[,] state = new int[COUNT, COUNT];  // 여기보세요
    public void InitState() {
        // 2차원 배열을 0 ~ 24 로 채우는 코드: 게임판의 블럭이 섞여있지 않은 상태
        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                state[y, x] = y * COUNT + x;
            }
        }
    }

    public void InitPanel() {
        grid = new Grid();
        for (int i = 0; i < COUNT; i++) {
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
        }
        Content = grid;
    }

    public void MakeGridImage() {
        Uri uri = new Uri("D3_ex/totoro.jpg", UriKind.Relative);
        BitmapImage bm = new BitmapImage(uri);

        bw = (int)((int)bm.PixelWidth / COUNT);
        bh = (int)((int)bm.PixelHeight / COUNT);

        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                // state 배열에 있는 정보를 가지고 블럭 선택
                if (state[y, x] != EMPTY) {
                    int bno = state[y, x];

                    // bno 가 8 이라면 8번 블럭의 x, y 를 알아야 합니다.
                    int bx = bno % COUNT; // 3
                    int by = bno / COUNT; // 1

                    // 아래 Int32Rect 의 인자만 변경
                    Int32Rect rc = new Int32Rect(bx * bw, by * bh, bw, bh); // <= 핵심
                    CroppedBitmap cb = new CroppedBitmap(bm, rc);

                    Image img = new Image();
                    img.Source = cb;
                    img.Stretch = Stretch.Fill;
                    img.Margin = new Thickness(0.5);

                    Grid.SetRow(img, y);
                    Grid.SetColumn(img, x);

                    grid.Children.Add(img);
                }  // end of if (state[y, x] != EMPTY)
            }  // end of for (int x = 0; x < COUNT; x++)
        }  // end of for (int y = 0; y < COUNT; y++)
    }

    public MainWindow() {
        InitState();
        InitPanel();
        MakeGridImage();
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • 퍼즐판의 상태를 나타내는 배열 추가
코드 보기
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System.IO;
using System.Media;
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

class MainWindow : Window {
    private int bw = 0;
    private int bh = 0;
    private const int COUNT = 5;
    private const int EMPTY = COUNT * COUNT - 1;
    private Grid grid = new();
    private int[,] state = new int[COUNT, COUNT];
    public void InitState() {
        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                state[y, x] = y * COUNT + x;
            }
        }
    }
    public void InitPanel() {
        grid = new Grid();
        for (int i = 0; i < COUNT; i++) {
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
        }
        Content = grid;
    }

    public void MakeGridImage() {
        Uri uri = new Uri("D3_ex/totoro.jpg", UriKind.Relative);
        BitmapImage bm = new BitmapImage(uri);

        bw = (int)((int)bm.PixelWidth / COUNT);
        bh = (int)((int)bm.PixelHeight / COUNT);

        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                if ( state[y, x] != EMPTY ) {
                    int bno = state[y, x];

                    int bx = bno % COUNT;
                    int by = bno / COUNT;

                    Int32Rect rc = new Int32Rect(bx * bw, by * bh, bw, bh);
                    CroppedBitmap cb = new CroppedBitmap(bm, rc);

                    Image img = new Image();
                    img.Source = cb;
                    img.Stretch = Stretch.Fill;
                    img.Margin = new Thickness(0.5);

                    Grid.SetRow(img, y);
                    Grid.SetColumn(img, x);

                    grid.Children.Add(img);
                }
            }
        }
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
        base.OnMouseLeftButtonDown(e);

        // 클릭 된 곳의 좌표를 grid 기반으로 얻기
        // Point pt = e.GetPosition(this); // 윈도우를 기준으로 좌표 얻기
        Point pt = e.GetPosition(grid); // grid 를 기준으로 좌표 얻기

        // 좌표를 가지고 몇번째 블럭을 클릭했는지 알아야 한다. -> 좌표 / grid 한 블럭의 크기
        int bx = (int)(pt.X / (grid.ActualWidth / COUNT));
        int by = (int)(pt.Y / (grid.ActualHeight / COUNT));

        // 게임판 밖이라면 무시
        if (bx < 0 || bx >= COUNT || by < 0 || by >= COUNT)
            return;

        if ( bx > 0 && state[by, bx-1] == EMPTY) // 왼쪽 조사
            MoveBlock(bx, by, bx-1, by); // (bx, by) 의 image 를 (bx-1, by)로이동
        else if (bx < COUNT-1 && state[by, bx + 1] == EMPTY) // 오른쪽 조사
            MoveBlock(bx, by, bx + 1, by);
        else if (by > 0 && state[by-1, bx] == EMPTY) // 위조사
            MoveBlock(bx, by, bx, by-1);
        else if (by < COUNT-1 && state[by + 1, bx] == EMPTY) // 아래조사
            MoveBlock(bx, by, bx, by + 1);
        else {
            // 4방향 모두 이동할수 없다면 "삑"
            SystemSounds.Beep.Play();
            return;
        }
    }

    public void MoveBlock(int x, int y, int tx, int ty) {
        // #1. Grid 의 x,y 위치에 있는 image 객체의 참조 얻기
        // var img = grid.GetChild(x, y); // 이런 메소드가 없습니다.

        // Grid 의 모든 자식을 순회 하면서 조사할수 밖에 없습니다.
        // => 이부분이 Grid 의 단점
        Image img = new();

        foreach( var e in grid.Children ) {
            Image i = (Image)e;
            if (Grid.GetRow(i) == y && Grid.GetColumn(i) == x)
                img = (Image)e;
        }

        // #2. image 객체의 Grid 위치 속성을 tx, ty 로 변경
        Grid.SetRow(img, ty);
        Grid.SetColumn(img, tx);

        // #3. state 배열의 상태도 변경해야 합니다.
        // state[y, x] <=> state[ty, tx] 를 서로 교환
        int tmp = state[y, x];
        state[y, x] = state[ty, tx];
        state[ty, tx] = tmp;
    }

    public MainWindow() {
        InitState();
        InitPanel();
        MakeGridImage();
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • 마우스 클릭 이벤트 구현
    1. 클릭한 곳의 좌표를 얻는다.
    2. 좌표를 가지고 몇 번째 블럭을 클릭했는지 알아야 한다.
    3. 게임판 밖이라면 무시한다.
    4. 빈칸과 인접한 블럭이 아니라면 무시한다.
    5. 빈칸과 인접한 블럭이라면 빈칸과 클릭한 블럭의 위치를 서로 바꿔준다.
    6. 블럭이 이동되었다면 다 맞추었는지 확인
  • TODO
    1. 다 맞추었는지 확인하는 기능 구현: 다 맞추면 메시지 출력
    2. 마우스 오른쪽 버튼 누르면 게임판 섞기
      • 다시 맞출 수 있도록 state[] 2차원 배열 섞기
코드 보기
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
using System.IO;
using System.Media;
using System.Printing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

class MainWindow : Window {
    private int bw = 0;
    private int bh = 0;
    private const int COUNT = 5; // 여기만 변경하면 블럭 갯수 변경됩니다.
    private const int EMPTY = COUNT * COUNT - 1;
    private Grid grid = new();
    private int[,] state = new int[COUNT, COUNT];
    public void InitState() {
        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                state[y, x] = y * COUNT + x;
            }
        }
    }
    public void InitPanel() {
        grid = new Grid();
        for (int i = 0; i < COUNT; i++) {
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
        }
        Content = grid;
    }
    public void MakeGridImage() {
        Uri uri = new Uri("D3_ex/totoro.jpg", UriKind.Relative);
        BitmapImage bm = new BitmapImage(uri);

        bw = (int)((int)bm.PixelWidth / COUNT);
        bh = (int)((int)bm.PixelHeight / COUNT);

        for (int y = 0; y < COUNT; y++) {
            for (int x = 0; x < COUNT; x++) {
                if (state[y, x] != EMPTY) {
                    int bno = state[y, x];

                    int bx = bno % COUNT;
                    int by = bno / COUNT;

                    Int32Rect rc = new Int32Rect(bx * bw, by * bh, bw, bh);
                    CroppedBitmap cb = new CroppedBitmap(bm, rc);

                    Image img = new Image();
                    img.Source = cb;
                    img.Stretch = Stretch.Fill;
                    img.Margin = new Thickness(0.5);

                    Grid.SetRow(img, y);
                    Grid.SetColumn(img, x);

                    grid.Children.Add(img);
                }
            }
        }
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
        base.OnMouseLeftButtonDown(e);

        Point pt = e.GetPosition(grid);

        int bx = (int)(pt.X / (grid.ActualWidth / COUNT));
        int by = (int)(pt.Y / (grid.ActualHeight / COUNT));

        if (bx < 0 || bx >= COUNT || by < 0 || by >= COUNT)
            return;

        if (bx > 0 && state[by, bx - 1] == EMPTY)
            MoveBlock(bx, by, bx - 1, by);
        else if (bx < COUNT - 1 && state[by, bx + 1] == EMPTY)
            MoveBlock(bx, by, bx + 1, by);
        else if (by > 0 && state[by - 1, bx] == EMPTY)
            MoveBlock(bx, by, bx, by - 1);
        else if (by < COUNT - 1 && state[by + 1, bx] == EMPTY)
            MoveBlock(bx, by, bx, by + 1);
        else {
            SystemSounds.Beep.Play();
            return;
        }
    }

    public void MoveBlock(int x, int y, int tx, int ty) {
        Image img = new();
        foreach (var e in grid.Children) {
            Image i = (Image)e;
            if (Grid.GetRow(i) == y && Grid.GetColumn(i) == x)
                img = (Image)e;
        }

        Grid.SetRow(img, ty);
        Grid.SetColumn(img, tx);

        int tmp = state[y, x];
        state[y, x] = state[ty, tx];
        state[ty, tx] = tmp;
    }
    private Random random = new Random();
    public void ShuffleMove() {
        int size = 5;
        int emptyValue = 24;

        // 1️⃣ 완성 상태로 초기화 (0~24)
        int value = 0;
        for (int r = 0; r < size; r++) {
            for (int c = 0; c < size; c++) {
                state[r, c] = value;
                value++;
            }
        }

        // 2️⃣ 셔플 (항상 풀 수 있도록 실제 이동만 수행)
        int shuffleCount = 1000;
        int emptyRow = 4; // 24는 마지막 위치
        int emptyCol = 4;

        for (int i = 0; i < shuffleCount; i++) {
            List<(int r, int c)> moves = new List<(int, int)>();

            if (emptyRow > 0) moves.Add((emptyRow - 1, emptyCol));
            if (emptyRow < size - 1) moves.Add((emptyRow + 1, emptyCol));
            if (emptyCol > 0) moves.Add((emptyRow, emptyCol - 1));
            if (emptyCol < size - 1) moves.Add((emptyRow, emptyCol + 1));

            var move = moves[random.Next(moves.Count)];

            // swap
            state[emptyRow, emptyCol] = state[move.r, move.c];
            state[move.r, move.c] = emptyValue;

            emptyRow = move.r;
            emptyCol = move.c;
        }
    }
    protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) {
        base.OnMouseRightButtonDown(e);
        ShuffleMove(); // state 배열 섞기
        grid.Children.Clear(); // grid 의 기존 모든 Image 제거..
        MakeGridImage(); // state 배열을 사용해서 Grid 배치
    }

    public MainWindow() {
        InitState();
        InitPanel();
        MakeGridImage();
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        w.Show();

        Application app = new Application();
        app.Run();
    }
}
  • AI로 완성한 퍼즐 게임 코드
이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

C# 프로그래밍 (2)

C# 프로그래밍 (4)