이미지 회전에 따른 여백 생성!

 

우선은 코드만 올립니다..

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private string workFolder = Path.Combine(Application.StartupPath, "workFolder");
        private int sequence = 0;

        public Form1()
        {
            InitializeComponent();

            //이미지 확인을 위해 오토사이즈로 해줍니다.
            pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize;
            pictureBox2.SizeMode = PictureBoxSizeMode.AutoSize;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            //파일을 불러옵니다.
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                pictureBox1.Image = Image.FromFile(ofd.FileName);
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (Directory.Exists(workFolder) == false)
            {
                Directory.CreateDirectory(workFolder);
            }

            sequence++; //돌린 횟수! (파일명 겹치지 않도록 해줍니다.)
            Image originImage = pictureBox1.Image; //사용될 원본 이미지!

            for (int rotation = 1; rotation <= 360; ++rotation) //360도 회전시켜줍니다.
            {
                int maxAngle = 91;

                int width = originImage.Width;
                int height = originImage.Height;

                int rot; //항상 0 ~ 90도로 만들어줍니다.

                if (90 < rotation && rotation <= 181) //해당 구간에서는 반전이 필요합니다.
                {
                    rot = 180 - rotation;
                }
                else if (270 < rotation && rotation <= 360) //해당 구간에서는 반전이 필요합니다.
                {
                    rot = 360 - rotation;
                }
                else
                {
                    rot = rotation % maxAngle;
                }

                double widthRadian = rot * Math.PI / 180;
                double heightRadian = (90 - rot) * Math.PI / 180; //90도 기준으로 반전 시켜줍니다.

                double w = Math.Cos(widthRadian) * width + Math.Sin(widthRadian) * height;
                double h = Math.Cos(heightRadian) * width + Math.Sin(heightRadian) * height;

                //기존 이미지 기준으로 돌려줍니다.
                DrawImage(originImage,
                          pictureBox1,
                          rotation,
                          width,
                          height,
                          width,
                          height,
                          "origin_");

                //크기가 확장 된 이미지 기준으로 돌려줍니다.
                DrawImage(originImage,
                          pictureBox2,
                          rotation,
                          (int)w,
                          (int)h,
                          width,
                          height,
                          "extend_");
            }

            originImage.Dispose();

            Process.Start(workFolder);
        }

        private void DrawImage(Image originImage,
                                PictureBox pictureBox,
                                int rotation,
                                int width,
                                int height,
                                int centerX,
                                int centerY,
                                string imageType)
        {
            Color backgroundColor = Color.FromArgb(255, 255, 0, 0);      //빨강 배경 (알파값을 통해 투명하게 가능!)

            Bitmap bitmap = new Bitmap(width, height);
            Graphics graphics = Graphics.FromImage(bitmap);

            graphics.Clear(backgroundColor);                             //배경 칠해주기
            graphics.TranslateTransform(width / 2, height / 2);          //이미지 가운데로 이동
            graphics.RotateTransform(rotation);                          //가운데에서 회전
            graphics.DrawImage(originImage, centerX / -2, centerY / -2); //이미지를 가운데 그려줍니다.

            string fileName = string.Format("{0}_{1}{2}.png", sequence.ToString("D4"), imageType, rotation.ToString("D4"));
            string savePath = Path.Combine(workFolder, fileName);

            bitmap.Save(savePath);

            graphics.Dispose();
            bitmap.Dispose();

            pictureBox.Image = Image.FromFile(savePath);
            pictureBox.Update();
        }
    }
}

 

 

간단한 용어 정리

Invoke : 메인 쓰레드에서 동작하도록 해주는 기능 (다른 스레드 => 메인UI 스레드 동작 시켜줌)

InvokeRequired : Invoke가 필요한 상황인지 체크. (메인UI 스레드 != 현재 실행중인 스레드 체크)

 

Windows Form에서 UI는 메인 스레드에서만 변경이 가능하며 그 외의 접근에 대해서는 예외가 발생됩니다.

 

우선 사용 방법은 다음과 같습니다. 방법1 혹은 방법2를 참고하여 사용하면 됩니다.

(예외와 주의 사항은 더 아래쪽에 적어두겠습니다.)

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form3 : Form
    {
        //방법2를 위한 delegate ******************
        private delegate void SetLabelDelegate();
        private SetLabelDelegate setLabelDelegate;
        //****************************************

        private string message1 = "";
        private string message2 = "";
        private string message3 = "";
        
        public Form3()
        {
            InitializeComponent();

            //방법2를 위한 delegate 등록 *************
            setLabelDelegate += SetLabel;
            //****************************************
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread thread = new Thread(ChangeUI);

            message1 = "mainThread " + AppDomain.GetCurrentThreadId().ToString();
            thread.Start();
        }

        private void ChangeUI ()
        {
            message2 += "subThread " + AppDomain.GetCurrentThreadId().ToString();

            if (InvokeRequired)
            {
                //방법1. MethodInvoker, delegate 조합
                Invoke((MethodInvoker)delegate
                {
                    message3 = "InvokeThread " + AppDomain.GetCurrentThreadId().ToString();
                    SetLabel();
                });

                //방법2. delegate 호출
                //Invoke(setLabelDelegate);
            }
            else
            {
                SetLabel();
            }
        }

        private void SetLabel ()
        {
            label1.Text = message1;
            label2.Text = message2;
            label3.Text = message3;
        }
    }
}

 

결과 (SetLabel은 메인쓰레드 7724로 실행)

 

 

오류 예시.

[System.InvalidOperationException - Cross Thread]

: 메인 스레드가 아닌 다른 스레드에서 UI 변경을 한 경우 발생

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread thread = new Thread(ChangeUI);

            MessageBox.Show(AppDomain.GetCurrentThreadId().ToString()); //main thread number
            thread.Start();
        }

        private void ChangeUI ()
        {
            MessageBox.Show(AppDomain.GetCurrentThreadId().ToString()); //other thread number
            label1.Text = "1";
        }
    }
}

 

결과 : 당연하게 오류 발생

 

 

주의 사항)

 쓰레드 번호를 확인할 수 있도록 코드를 작성해놨습니다.

이것으로 현재 함수가 어떤 쓰레드에서 실행 중인지 볼 수 있는데,

Invoke를 통해 MainThread -> SubThread -> MainThread 방식의 흐름이 가능합니다.

반드시 필요한 곳에서만 Invoke를 사용해야 올바른 비동기 코드가 진행이 될 수 있습니다.

잘 못된 코드와 올바른 코드 예시를 남깁니다.

 

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread validInvokeThread = new Thread(ChangeUI);
            validInvokeThread.Start();

            //이렇게 호출되면 안됨.
            //Thread invalidInvokeThread = new Thread(ChangeUI2);
            //invalidInvokeThread.Start();
        }

        private void ChangeUI()
        {
            //스레드 동작과 메인UI 스레드가 원하는대로 잘 동작하게 됩니다.
            SetLabel(label1, AppDomain.GetCurrentThreadId().ToString());
            Thread.Sleep(1000);
            SetLabel(label2, AppDomain.GetCurrentThreadId().ToString());
            Thread.Sleep(1000);
            SetLabel(label3, AppDomain.GetCurrentThreadId().ToString());
        }

        private void ChangeUI2()
        {
            //이렇게 사용하면 MainThread에서 모든 것을 처리해주게 됩니다.
            //즉 "스레드의 사용 의미가 없다" 입니다.
            if (InvokeRequired)
            {
                Invoke((MethodInvoker)delegate
                {
                    ChangeUI2();
                });
            }
            else
            {
                SetLabel(label1, AppDomain.GetCurrentThreadId().ToString());
                Thread.Sleep(1000);
                SetLabel(label2, AppDomain.GetCurrentThreadId().ToString());
                Thread.Sleep(1000);
                SetLabel(label3, AppDomain.GetCurrentThreadId().ToString());
            }
        }

        private void SetLabel(Label label, string text) //delegate로 함수를 만들어서 처리도 가능
        {
            if (InvokeRequired)
            {
                Invoke((MethodInvoker)delegate
                {
                    label4.Text = "메인스레드 : " + AppDomain.GetCurrentThreadId().ToString();
                    label.Text = text;
                });
            }
            else
            {
                label4.Text = "메인스레드 : " + AppDomain.GetCurrentThreadId().ToString();
                label.Text = text;
            }
        }
    }
}

 

ChangeUI (UI는 메인에서, Sleep은 다른 스레드에서 처리)

ChangeUI2 (메인 스레드에서 처리. Sleep에 의해 멈추기도함)

 

GraphicsPath 를 이용하여 둥근 버튼, 별표 등 다양한 형태의 도형을 만들 수 있습니다.

둥근 버튼

 

1. 새 항목 추가로 클래스를 만들어줍니다.

 

2. 아래와 같이 Button을 상속 받고 로직을 작성해줍니다.

  (path 첫 위치에 따라 회전각도가 달라집니다. 회전은 0도 = 12시 방향이고, 시계방향으로 돕니다!)

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace MyButton
{
    class CustomButton : Button
    {
        public int Radius { get; set; } //외부에서 크기를 받습니다.
        public bool IsMaximum { get; set; } //외부에서 최대 사이즈를 받습니다.

        public CustomButton ()
        {
            Radius = 20;
            IsMaximum = true;

            this.BackColor = Color.Red; //기본 색을 지정해줍니다.
            this.FlatStyle = FlatStyle.Flat; //버튼을 플랫하게 만들어줍니다.
            this.FlatAppearance.BorderSize = 0; //보더가 지저분해지기에 제거해줍니다.
        }

        protected override void OnPaint(PaintEventArgs p)
        {
            GraphicsPath path = new GraphicsPath();
            Rectangle rectangle = ClientRectangle;

            int radius = Math.Min(this.Height, Radius); //Height를 넘어가면 모양이 이상해집니다!

            if (IsMaximum) //최대 사이즈인 경우는 Height로!
            {
                radius = this.Height;
            }

            int x = rectangle.X;
            int y = rectangle.Y;
            int width = rectangle.Width;
            int height = rectangle.Height;

            //그리는 순서가 굉장히 중요합니다!
            path.AddArc(x, y, radius, radius, 180, 90); //왼쪽 상단
            path.AddArc(x + width - radius, y, radius, radius, 270, 90); //오른쪽 상단
            path.AddArc(x + width - radius, y + height - radius, radius, radius, 0, 90); //오른쪽 하단
            path.AddArc(x, y + height - radius, radius, radius, 90, 90); //왼쪽 하단

            path.CloseAllFigures();

            this.Region = new Region(path); //영역을 둥근형태로 만들어줍니다.

            base.OnPaint(p);
        }
    }
}

 

프로젝트를 "빌드" 하면 아래와 같이 구성요소에서 재정의한 버튼을 볼 수 있습니다.

해당 버튼을 알맞게 배치하고, 값을 설정하여 사용합시다!

디자인 문서개요는 컨트롤들의 순서 등 정리가 아주 편리합니다. (특히 도킹 관련해서 순서 지정이 가능!)

(+ 꿀팁 : 컨트롤에 포커싱 있는 상태에서 ESC 키를 통해 상위 컨트롤로 선택이 가능합니다.)

 

보기 => 다른 창 => 문서 개요  를 통해 복잡한 오브젝트를 쉽게 볼 수 있습니다.

 

문서 개요 매뉴

 

왼쪽 문서 개요 뷰에서 요소의 순서를 바꾸거나 삽입 / 제거 도 쉽답니다.

 

간단한 구조가 마음에 들어 까먹기 전에 다시 작성해두기!

using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Forms;

namespace LogTest
{
    public enum LogLevel
    {
        Debug, Info, Error
    }

    public static class Log
    {
        private static LogLevel logLevel;
        private static string logPath;

        public static void Init(LogLevel level) //최초 한번 실행해주기!
        {
            logLevel = level;

            //폴더 등 설정
            logPath = Path.Combine(Application.StartupPath, "test.log");
            //기존 불필요한 로그 삭제하기.
        }

        public static void Write(string message, LogLevel level, [CallerMemberName] string callFunction = "")
        {
            if (logLevel <= level)
            {
                string type = level.ToString();

                string logContent = string.Format("[{0}] {1} ({2}) : {3}{4}",
                                                type, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss"), callFunction, message, Environment.NewLine);

                File.AppendAllText(logPath, logContent);
            }
        }
    }
}

세 개의 버튼을 만들고 테스트 해봅시다.

 

프로그램 중간에 종료를 시켜야 하는 경우 위의 3가지 방식을 사용할 수 있습니다.

각 특징은 아래와 같습니다.

 

Close() : 해당되는 폼만 닫습니다.

                

Application.Exit() : 다른 서브 폼에서 호출 해도 메인 폼의 Close()까지 동작되게 됩니다.

 

Environment.Exit(0) : 이 곳에서 바로 종료됩니다.

 

using System;
using System.Windows.Forms;

namespace CloseTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Close();
            //이 후 로직도 타게 됩니다.
            //무언가 로직 실행... (+ FormClosing)
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Application.Exit();
            //이 후 로직도 타게 됩니다.
            //무언가 로직 실행... (+ FormClosing)
        }

        private void button3_Click(object sender, EventArgs e)
        {
            Environment.Exit(0);
            //바로 종료 됩니다. (FormClosing을 타지 않습니다.)
            //실행되지 않는 이 후 로직!
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            //Close(), Application.Exit() 시에 이 곳까지 도달합니다.

            e.Cancel = true; //프로그램이 종료되지 못하도록 막습니다.
        }
    }
}

base64 바이트를 이용한 파일 읽기 / 생성

 

base64를 통해 파일을 바이트 혹은 "텍스트" 형태로 주고 받을 수 있습니다.

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

namespace Base64
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            //텍스트가 길어질 수 있으니 늘려줍니다.
            //혹은 파일을 읽어도 괜찮겠네요.
            textBox1.MaxLength = 100000000;
        }

        private void button_fileToBase64_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                string filePath = openFileDialog.FileName;
                byte[] fileBytes = File.ReadAllBytes(filePath); //바이트로 파일을 읽어온 후
                char[] fileToBase64 = new char[(int)Math.Ceiling(fileBytes.Length * 1.5)]; //base64는 33% 증가!
                Convert.ToBase64CharArray(fileBytes, 0, fileBytes.Length, fileToBase64, 0); //base64로 만들어줍니다.

                MessageBox.Show(fileBytes.Length + " " + fileToBase64.Length);

                Clipboard.SetText(new string(fileToBase64)); //클립보드에 복사해줍니다. string으로 만들어서!
            }
        }

        private void button_base64ToFile_Click(object sender, EventArgs e)
        {
            string text = textBox1.Text;
            byte[] textBytes = Convert.FromBase64String(text); //위의 string을 다시 base64 byte로 만들어줍니다.

            string folder = Directory.GetCurrentDirectory();
            string filePath = Path.Combine(folder, "file");
            File.WriteAllBytes(filePath, textBytes); //해당 값으로 파일을 만들어줍니다.

            Process.Start(folder); //파일 생성 확인용 폴더 띄워주기
        }
    }
}

 

Alt + Tab 시 Form이 화면에 노출 됩니다.

 

아래 소스를 통해 탭에서 해당 폼을 볼 수 없도록 만들 수 있습니다.

Form3이 있지만 테스크 변경에서 보이지 않게 됩니다.

using System.Windows.Forms;

namespace DrawString
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();

            this.ShowInTaskbar = false;
            this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
        }

        //this.FormBorderStyle = FormBorderStyle.FixedToolWindow; 와 동일한 동작을 합니다.
        //protected override CreateParams CreateParams
        //{
        //    get
        //    {
        //        CreateParams cp = base.CreateParams;
        //        cp.ExStyle |= 0x80;
        //        return cp;
        //    }
        //}
    }
}

    //아래 두형태에 대해서는 Alt+Tab에서 보이지 않게 되는군요!
    public enum FormBorderStyle
    {
        ...
        // 요약:
        //     크기를 조정할 수 없는 도구 창 테두리입니다. 사용자가 ALT + TAB을 누를 때 표시 되는 창 또는 작업 표시줄에는 도구 창이 나타나지
        //     않습니다. 지정 하는 폼 있지만 System.Windows.Forms.FormBorderStyle.FixedToolWindow 일반적으로에
        //     표시 되지 않은 작업 표시줄을 확인 해야는 System.Windows.Forms.Form.ShowInTaskbar 속성이 false, 기본값
        //     이므로, true합니다.
        FixedToolWindow = 5,
        //
        // 요약:
        //     크기 조정 가능한 도구 창 테두리입니다. 사용자가 ALT + TAB을 누를 때 표시 되는 창 또는 작업 표시줄에는 도구 창이 나타나지 않습니다.
        SizableToolWindow = 6
    }

 

+ Recent posts