Custom URI 를 만들어 웹페이지에서 내 PC에 있는 exe 파일을 실행시키는 방법입니다.

아래와 같이 작동 됩니다.

 

웹페이지에서 아이디 비밀번호와 같은 입력 값을 넘겨주며 .exe 파일을 실행시킵니다.
실행 버튼을 누른 결과가 출력됩니다.

 (주의 사항이 하나 있는데 위와 같이 변환 된 결과를 보면 웹 url에서 특수 문자가 다르게 취급됩니다.

UTF-8 과 System.Net.WebUtilty.UrlDecode 기능을 잘 섞어서 필요한 내용으로 만들어주면 되겠습니다.)

 

 

 먼저 레지스트리에 해당 파일을 실행할 수 있도록 등록해주어야 합니다.

 exe파일 실행 시 Main(string[] args) 부분에 대한 처리가 필요합니다.

 차근차근 해봅시다.

 

  1. 윈도우키 + R을 눌러 "실행" 창을 띄워줍니다. (혹은 시작->실행 검색)

  2. regedit 입력을 해줍니다.

실행 창에서 regedit 입력 후 확인

  3. HKEY_CLASSES_ROOT 오른쪽 클릭 후 새로 만들기 -> 키 를 선택해줍니다.

   이름은 "Test"로 설정해주겠습니다.

  4. 아래와 같이 키를 생성해줍니다.

    Test에서 shell, shell 에서 open, open 에서 command까지 생성해줍니다.

  5. Test 에서 새로 만들기 -> 문자열 값 을 통해 새 값을 만들어줍니다.

   해당 값의 이름을 URL Protocol 로 바꿔줍니다.

  6. 이번엔 command 쪽의 기본 값을 수정해줍니다. (기본 값을 더블 클릭 해줍니다.)

  아래와 같이 문자열 편집이 뜹니다. exe파일의 경로가 되겠습니다.

  "C:\Test\test.exe" "%1"

 C드라이브 테스트 폴더의 test.exe 실행을 하고, %1은 실행 시 매개변수 값을 하나 줄 수 있다는 의미입니다.

  7. 비주얼 스튜디오에서 다음과 같은 코드를 작성하고 exe 파일을 생성합니다.

using System;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("받아 온 값");

            if (args.Length > 0)
            {
                Console.WriteLine(args[0]); //받아온 값을 보여줍니다.
            }

            Console.ReadLine(); //값을 보기 위해 대기
        }
    }
}

 

  8. 해당 파일을 C:\Test\Test.exe 로 옮겨줍시다.

  9. 웹에서 실행합니다. 간단하게 url 창에서 실행해보겠습니다.

  test://hihi ook

  원하는 값을 입력해줍니다. 그리고 엔터를 치면

  10. 짜잔 실행이 됩니다.

    저 띄어쓰기 부분은 %20 으로 나오고 맨 마지막에도 / 가 자동으로 붙게 됩니다.

   우선 간단하게 값을 받아오고 실행에 성공을 하였습니다. 

받아온 값을 원하는 값으로 변경해서 사용해주면 됩니다!

 

 설명이 너무 길어져서 나머지는 예제 파일로 남겨두겠습니다.

비주일 스튜디오 2019로 작업한 파일과 레지스트리 생성, test.exe 파일이 들어있습니다.

웹에서 어떻게 호출하는지와 C#에서 값을 어떻게 받아오는지도 있으니 필요하면 확인해보시길!

커스텀 URI 만들기.zip
0.83MB

 

코드 정리도 따로 올립니다.

html 코드

 

[파일명]

custom uri.html

<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <script>
   
   function play ()
   {
    let id = document.getElementById("ID").value;
    let pw = document.getElementById("PW").value;

    let url = "Test://" + id + "&" + pw + "&"; // 레지스트리에 등록 된 Test:// 호출 부분입니다.
    //마지막에 &를 추가해줬는데 끝 부분에 / 와 같은 문자가 붙을 수 있어 경계 탐지용으로..
    let exec = document.createElement("a");
    exec.setAttribute("href", url);
    exec.click();
   }   

  </script>
 </head>
 <body>
  <div> 아이디 <input id="ID" type="text" value="testID!@"></div>
  <div> 비밀번호 <input id="PW" type="text" value="!@#$%^&*()"></div>
  <div> <button id="call" onclick="play()"> 실행 </button> </div>
 </body>
</html>

 

WindowsForm

[Program.cs]

using System;
using System.Windows.Forms;

namespace Custom_uri_Example
{
    static class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
         
            if (args.Length > 0) //외부 호출하여 인자값이 있는 경우
            {
                Application.Run(new Form1(args));
            }
            else //바로 .exe 실행
            {
                Application.Run(new Form1());
            }
        }
    }
}

 

[Form1.cs]

using System.Windows.Forms;

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

        public Form1(string[] args) : this()
        {
            textBox_all.Text = args[0];

            string data = args[0].Replace("test://", ""); //앞에 불필요한 문자열은 지워줍니다.

            string[] separate = data.Split('&'); //사전에 약속 된 구분자로 나눠줍니다.

            string utf_id = separate[0];
            string utf_pw = separate[1];

            textBox_utf_id.Text = utf_id;
            textBox_utf_pw.Text = utf_pw;

            //웹에서 받아온 문자 중 특수문자가 다른 경우가 있을 때 WebUtility를 사용해줍니다.
            //System.Net 사용이 안되면 참조 추가에서 System.Net을 추가해주면 됩니다.
            string web_id = System.Net.WebUtility.UrlDecode(separate[0]);
            string web_pw = System.Net.WebUtility.UrlDecode(separate[1]);

            textBox_web_id.Text = web_id;
            textBox_web_pw.Text = web_pw;
        }
    }
}

C#은 기본적으로 StructLayout 가 정해져 있습니다.

클래스는 [StructLayout(LayoutKind.Auto, Pack = 0)]

구조체는 [StructLayout(LayoutKind.Sequential, Pack =0)]

 

평소엔 신경쓰지 않아도 되지만 C, C++ 혹은 서버와 통신 등 메모리를 맞춰줘야 될 때

사용되는 기능이니 이런 것이 있다는 것만 알면 될거 같습니다.

아래 코드는 C#으로 C, C++ 포인터를 흉내내어 메모리 참조를 해본 코드입니다.

 

using System;
using System.Runtime.InteropServices; // [StructLayout ] 선언에 필요합니다.

//참고 설명 https://www.csharpstudy.com/DevNote/Article/10

//*****************************************************************
//
//  기본값
//  class  : [StructLayout(LayoutKind.Auto, Pack = 0)]
//  struct : [StrcutLayout(LayoutKind.Sequential, Pack = 0)]
//
//  Pack : 0이면 자동 세팅에 따릅니다. (0, 1, 2, 4, 8, 16... 128 까지 2의 배수만 가능합니다.)
//         1 이상이면 Pack과 타입 크기 중 가장 큰 단위에 따라 패딩을 넣어줍니다.
//      
//  ex)
//      [StructLayout(LayoutKind.Sequential, Pack = 0)]
//      struct MyStruct
//      {
//          public int i;     // 4
//          public double d;  // 8
//          public byte b;    // 1
//      }
//
//      가장 큰 사이즈에 맞추어 크기가 조절 됩니다.
//      Pack = 0 이면 가장 큰 사이즈 byte
//      sizeof(MyStruct) == 24
//      
//      i  = (0~3)
//    패딩 = (4~7)
//      d  = (8~15)
//      b  = (16)
//    패딩 = (17~23)
//
//      Pack = 1 이면 1에 가까운 타입에 맞추어집니다. 즉 byte 중심이 되며
//      i = (0~3), d = (4~11), b = (12) 가 되어 0~12까지 size = 13이 됩니다.
//
// LayoutKind.Sequential    : 메모리상에 순차적으로 저장합니다.
//            Auto          : 힙 영역에 자동으로 저장합니다.
//            Explicit      : 직접 메모리 영역을 지정해줍니다.
//                            변수에 [FieldOffset()] 식으로 지정해주어야 합니다.
//
//****************************************************************


namespace StructLayout
{
    class Program
    {
        [StructLayout(LayoutKind.Explicit, Pack = 1)] //int형 4에 맞춰집니다.
        public struct Test
        {
            [FieldOffset(0)]
            public int value; // 4 (0000 0000, 0000 0000, 0000 0000, 0000 0000)

            [FieldOffset(0)]
            public byte value1; // (0000 0000 ____ ____ ____ ____ ____ ____)

            [FieldOffset(1)]
            public byte value2; // (____ ____ 0000 0000 ____ ____ ____ ____)

            [FieldOffset(2)]
            public byte value3; // (____ ____ ____ ____ 0000 0000 ____ ____)

            [FieldOffset(3)]
            public byte value4; // (____ ____ ____ ____ ____ ____ 0000 0000)
        }

        //c언어의 포인터를 흉내낼 수 있습니다!
        static unsafe void Main(string[] args) //unsafe : 포인터 사용지 설정이 필요합니다.
        {
            Test test = new Test();

            Console.WriteLine("sizeof(TEST) : {0}", sizeof(Test));

            //c, c++언어의 포인터
            int* startAddress = (int*)&test;

            //1100 1100, 0000 0100, 0000 0001, 0000 0001
            test.value = (1 << 31) | (1 << 30) | (1 << 28) | (1 << 27) | //1100 1100 ____ ____ ____ ____ ____ ____
                         (1 << 18) |                                     //1100 1100 0000 0100 ____ ____ ____ ____
                         (1 << 8)  |                                     //1100 1100 0000 0100 0000 0001 ____ ____
                         1;                                              //1100 1100 0000 0100 0000 0001 0000 0001

            Console.WriteLine("test.value : {0}", Convert.ToString(test.value, 2)); //2진수로 출력
            Console.WriteLine("test.value1 : {0}", Convert.ToString(test.value1, 2)); //2진수로 출력 0000 0001
            Console.WriteLine("test.value3 : {0}", Convert.ToString(test.value2, 2)); //2진수로 출력 0000 0001
            Console.WriteLine("test.value2 : {0}", Convert.ToString(test.value3, 2)); //2진수로 출력 0000 0100
            Console.WriteLine("test.value4 : {0}", Convert.ToString(test.value4, 2)); //2진수로 출력 1100 1100
        }
    }
}

 

 

결과 값 2진수로 표현하였고 앞의 0은 사라집니다.

 

FormBorderStyle = None 을 주고 사용자 지정 타이틀을 만들다보면 최대화 시켰을 때 맨 아래 작업 표시줄이 가려집니다.

 

(왼쪽이 평상시 상태, 오른쪽은 가려진 형태)

 

이에따라 현재 스크린에 따른 최대화 사이즈 지정이 필요합니다.

 

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

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

        private void button1_Click(object sender, EventArgs e)
        {
            if (this.WindowState == FormWindowState.Normal)
            {
                Size size = new Size();

                //현재 폼이 가장 가까운 모니터에 대한 작업 영역 크기를 가져옵니다.
                size.Width = Screen.FromControl(this).WorkingArea.Width;
                size.Height = Screen.FromControl(this).WorkingArea.Height;

                this.MaximumSize = size;
                this.WindowState = FormWindowState.Maximized;
            }
            else
            {
                this.WindowState = FormWindowState.Normal;
            }
        }
    }
}

 

 

 

 

 

======================================================================================

이전 코드...

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

namespace WindowsFormsApp6
{
    public partial class Form1 : Form
    {
        private Point prePosition;

        public Form1()
        {
            InitializeComponent();
        }

        private void button_close_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void button_sizeChange_Click(object sender, EventArgs e)
        {
            if (this.WindowState == FormWindowState.Normal)
            {
                SetMaximize();
            }
            else
            {
                SetNormal();
            }
        }

        private void SetNormal ()
        {
            this.Location = prePosition; //크기 변경 시 원 위치로 복귀
            this.WindowState = FormWindowState.Normal;
        }

        private void SetMaximize ()
        {
            Screen[] screens = Screen.AllScreens;
            int length = screens.Length;
            int area;

            for (area = 0; area < length; ++area) //모든 모니터 영역 확인
            {
                if (screens[area].WorkingArea.Contains(this.Location))
                {
                    break;
                }
            }

            //화면 밖인 경우 첫번째 모니터로 해줍니다.
            if (length <= area) 
            {
                area = 0;
            }

            prePosition = this.Location; //노멀화 시킬 때 이동시킬 좌표값.

            //화면 기준점으로 이동 후 최대화 시켜주어야 정상적인 결과를 얻습니다.
            this.Location = new Point(screens[area].Bounds.X, screens[area].Bounds.Y);

            this.MaximizedBounds = new Rectangle(MaximizedBounds.X,
                                                 MaximizedBounds.Y,
                                                 screens[area].WorkingArea.Width,  //width 와 height를 바꿔줍니다.
                                                 screens[area].WorkingArea.Height);

            this.WindowState = FormWindowState.Maximized; //최대화
        }
    }
}

인덱서는 클래스에 [ ] 를 통해 어떠한 값을 설정하거나 가져오는 방법입니다.

 

프로퍼티처럼 get, set 설정을 해줘야 합니다.

 

public 리턴타입 this [타입 변수명]  으로 시작합니다. (int, string 모두 가능합니다!)

 

(복잡해질 수 있어 안쓰겠거니 했는데 많은 변수에 값을 할당할 때

 

일일히 값 체크를 하며 하드코딩 하는 형태가 되는데 사용해보니 조금 더 머리가 덜 아픈 느낌이 듭니다.)

 

 

using System;

namespace ConsoleApp2
{
    class A
    {
        public int a;
        public int b;
        public int c;

        public int this [int index] //인덱서 설정 indexer
        {
            get
            {
                switch (index)
                {
                    case 0:
                        return a;
                    case 1:
                        return b;
                    case 2:
                        return c;
                    default:
                        return 0;
                }
            }
            set
            {
                switch (index)
                {
                    case 0:
                        a = value;
                        break;
                    case 1:
                        b = value;
                        break;
                    case 2:
                        c = value;
                        break;
                    default:
                        break;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            A a = new A();
            int[] values = { 33, 28, 90 };

            for (int i = 0; i < 3; ++i)
            {
                a[i] = values[i]; //indexer set

                Console.WriteLine(a[i]); //indexer get
            }

            Console.WriteLine("a = {0}, a[0] = {1}", a.a, a[0]);
            Console.WriteLine("b = {0}, a[1] = {1}", a.b, a[1]);
            Console.WriteLine("c = {0}, a[2] = {1}", a.c, a[2]);
        }
    }
}

 

해당 변수를 인덱스화 시켜서 사용하는 방법! 결과입니다.

참고할거라서 먼저 코드부터 올리겠습니다 ' - '!  (설명은 밑에 간단히)

 

(+ 빈 값을 자동으로 제거해주는 방법도 있습니다

JsonConvert.SerializeObject(data,  //데이터
                            Formatting.Indented, //줄 바꿈 여부
                            new JsonSerializerSettings
                            {
                              DefaultValueHandling = DefaultvalueHandling.Ignore, //기본 값 제거
                              NullValueHandling = NullValueHandling.Ignore //NULL 값 제거
                            };

/*
Formatting.None  : 줄 바꿈 없음 
ex) { "a"="asd"}

Formatting.Indented : 줄 바꿈
ex)
{
  "a" = "asd"
}

DefaultvalueHandling.Ignore : 문자 = null, 숫자 = 0 제거
NullValueHandling.Ignore    : 문자 or 숫자 = null 제거

*/

 

예제 코드

using System;
using System.Collections.Generic;
using Newtonsoft.Json;                  //JsonProperty, JsonConvert
using Newtonsoft.Json.Linq;             //JObject, JArray
using Newtonsoft.Json.Serialization;    //Serialize, Deserialize

namespace ConsoleApp2
{
    class Program
    {
        class TestClass
        {
            //이름이 동일한 곳으로 파싱됩니다.
            //[JsonProperty("a")] 
            public int a;

            //제이슨에서 xx변수를 b라고 호칭합니다.
            [JsonProperty("b")]
            public int xx;

            //리스트(배열) 가능!
            public List<string> c = new List<string>();
        }

        static void Main(string[] args)
        {
            //*********************직렬화*************************

            TestClass test = new TestClass();
            test.a = 1;
            test.xx = 2;
            test.c.Add("hi");
            test.c.Add("ok");

            string serialized = JsonConvert.SerializeObject(test);
            string message = "{\"a\":1,\"b\":2,\"c\":[\"hi\",\"ok\"]}";
            
            //xx가 JsonProperty에 의해 c로 변환 됩니다.
            Console.WriteLine("serialized : {0}", serialized);
            Console.WriteLine("message : {0}", message);

            //*****************  파싱/ 역직렬화 ********************

            //오브젝트 형태로 파싱하여 사용하기
            JObject jObject = JObject.Parse(serialized);
            JArray jArray = jObject["c"].ToObject<JArray>(); //JArray로 변환

            Console.WriteLine("JObject : {0} {1}", jObject["a"], jObject["b"]);
            Console.WriteLine("JArray : {0} {1}", jArray[0], jArray[1]);

            //역직렬화 후 사용하기
            TestClass testClass = JsonConvert.DeserializeObject<TestClass>(message);
            Console.WriteLine("JsonConvert.DeserializeObject a:{0} b:{1}", testClass.a, testClass.xx);
            Console.WriteLine("JsonConvert.DeserializeObject c[0]:{0} c[1]:{1}", testClass.c[0], testClass.c[1]);

            //역직렬화 후 사용하기 (중간에 값이 빠져도 역직렬화 가능합니다!)
            string message2 = "{ b:1 }";
            TestClass testClass2 = JsonConvert.DeserializeObject<TestClass>(message2);
            Console.WriteLine("JsonConvert.DeserializeObject a:{0} b:{1}", testClass2.a, testClass2.xx);
        }
    }
}

직렬화 / 파싱 / 역직렬화 결과

 

JSON : Java Script Object Notation    키와 값을 갖는 문자열 형태입니다.

ex)  { name : "이름" , age : "33" } 

 

웹상에서 데이터를 주고 받기 편하고 많이 사용합니다!

위의 코드에 기본적인 사용법은 적혀 있으니 참고하시기 바랍니다.

 

JObject 형태로 사용하거나 class로 직렬화/역직렬화 가능합니다.

 

클래스 없이도 JProperty 형태로 삽입 만들 수도 있답니다. (아니면 위의 message처럼 직접 써도 되구요~)

JObject obj = new JObject();
obj.Add(new JProperty("oo", "!!"));
Console.WriteLine(obj["oo"]); // !!

 

 

설치법

1. 비주얼스튜디오 "솔루션 NuGet 패키지 관리" 에서 다운 받을 수 있습니다.

2. Json 홈페이지에서 .dll 다운도 가능합니다.

 

 도구 -> NuGet 패키지 관리자 -> 솔루션용 NuGet 패키지 관리

 

찾아보기 -> Json 검색 -> 오른쪽에서 설치 -> 솔루션 탐색기 참조에 생성됩니다.

using Newtonsoft.Json을 사용 가능해집니다!

 

C# 윈도우 폼에서 자주 사용하는 뷰는

textBox,

ListBox,

ListView,

DataGridView

 

정도가 되겠습니다. 이 중 ListView가 무언가를 보여줄 때 가장 효과적인 것 같습니다.

기본 사용법을 살펴보겠습니다.

 

리스트뷰를 가져온 상태입니다. 처음엔 아무것도 없는 백지인 상태!

 

오른쪽 상단의 > 화살표를 누르면 위와 같이 팝업창이 뜹니다.

천천히 알아보겠습니다.

그 전에 가장 보편적인 형태로 보도록 <뷰:  LargeIcon> 을 Details로 바꿔줍니다.

(뷰에 관한 것은 나중에 보여드릴께요~)

 

짠~ 아무것도 변한게 없습니다...

이제 [열 편집] 을 눌러주겠습니다.

 

추가를 눌러주고 싶게 생긴 ColumnHeader 편집기

추가 한 3개만 눌러봅시다. 그러면!!

 

리스트뷰에서 볼 수 있는 헤더가 생성되었습니다. 

컬럼명을 수정해 봅시다. 오른쪽에 Text 속성을 변경하면 됩니다. 차례대로 1, 2, 3 !

 

좋습니다. 나머지 속성은 딱 보면 아시겠죠?

TextAlign : 텍스트 정렬 위치 (Left, Center, Right)

Width : 헤더의 넓이

ImageIndex와 ImageKey 는 나중에 Image 를 집어넣을 때 사용할 수 있고

DisplayIndex를 통해 위치를 변경해줄 수 있습니다. columnHeader3의 DisplayIndex를 0으로 잠깐 해보면

3, 1, 2 순서로 변경되었습니다.

(하지만 말 그대로 보여지는 순서일 뿐 실제로 위 헤더 인덱스는 [0] 1, [1] 2, [2] 3 상태로 존재하게 됩니다.)

 

이번엔 내용을 추가해보겠습니다.

다시 팝업 메뉴로 돌아가서 이번엔 [ 항목 편집 ] 을 눌러줍니다.

 

마찬가지로 추가를 눌러줍니다.

 

오른쪽의 SubItems 를 선택하면 아래와 같은 창이 뜹니다.

2개를 추가한 상태이고 오른쪽에 Text 가 비어있기에 아무것도 보이지 않습니다.

각각 1111, 2222로 주면 리스트뷰에서 해당 값을 볼 수 있습니다.

다만 맨 첫 번째 값은 무시가 되었는데 이 녀석은 바로 요기에 있답니다.

 

위의 과정을 코드로 작성하게 되면 아래와 같이 되겠습니다.

using System.Windows.Forms;

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

            ListViewItem item = new ListViewItem();
            item.Text = "0000";
            item.SubItems.Add("1111");
            item.SubItems.Add("2222");

            listView1.Items.Add(item); //해당 아이템 추가!
        }
    }
}

 

그리고 값을 접근하기 위해선 listView1.Items 의 배열을 통하면 됩니다.

위의 0000, 1111, 2222 값은

 

listView1.Items[0].SubItems[0].Text // 0000

listView1.Items[0].SubItems[1].Text // 1111

listView1.Items[0].SubItems[2].Text // 2222

 

이런식으로 접근할 수 있고 수정도 가능하답니다.

1편은 여기서 끝!

.ini 파일은 메모장으로도 열리고 initialization 을 의미 합니다.

초기화 할 때 사용할 영역(section), 키(key), 값(value)을 가지고 있습니다.

저처럼 쫄지마세요.. 되게 간단합니다!

 

Write할 때 딱 세 가지만 아시면 됩니다.

1. 섹션이 없다면 섹션이 생성되고 키도 없다면 생성 됩니다. (키, 값이 해당 섹션으로 추가 됩니다.)

2. 동일한 키가 있다면 덮어 씌여집니다.

3. 삭제는 null 값으로 써주면 됩니다. (키 삭제, 섹션 삭제 가능)

 

예제 코드와 결과입니다.

using System;
using System.IO;
using System.Text;
using System.Runtime.InteropServices; //DllImport

namespace ConsoleApp2
{
    class Program
    {
        [DllImport("kernel32.dll")]
        private static extern uint GetPrivateProfileString(string section,
                                                           string key,
                                                           string defaultValue, //키값이 없을 때의 기본 값
                                                           StringBuilder returnedString,
                                                           uint size,
                                                           string filePath);

        [DllImport("kernel32.dll")]
        private static extern bool WritePrivateProfileString(string section,
                                                             string key,
                                                             string value,
                                                             string filePath);

        static void Main(string[] args)
        {
            string filePath = Path.Combine(Environment.CurrentDirectory, "test.ini");

            //파일이 없다면 만들어줍니다. 해당 프로젝트 Debug 폴더에 위치합니다.
            if (File.Exists(filePath) == false)
            {
                File.Create(filePath);
            }

            WritePrivateProfileString("Section1", "key1", "value1", filePath);
            WritePrivateProfileString("Section1", "key2", "value2", filePath);
            WritePrivateProfileString("Section1", "key333", "ooo", filePath);

            System.Threading.Thread.Sleep(1000); //Write 이 후 바로 못 읽기에 시간을 줍니다..

            StringBuilder sb = new StringBuilder { Capacity = 100 }; //프로퍼티 초기화 방식

            GetPrivateProfileString("Section1", "key1", "Nothing..", sb, (uint)sb.Capacity, filePath);
            Console.WriteLine(string.Format("[Section1]'s key1 = {0}", sb.ToString())); //Section1의 key1 값을 가져옵니다.

            GetPrivateProfileString("Section2", "key11", "Nothing..", sb, (uint)sb.Capacity, filePath);
            Console.WriteLine(string.Format("[Section1]'s key11 = {0}", sb.ToString())); //Section2가 없으므로 Nothing..이 나옵니다.

            WritePrivateProfileString("Section1", "key333", null, filePath); //key333을 지웁니다.

            /*
            WritePrivateProfileString("Section1", null, null, filePath); //Section1 전체가 사라집니다.
            */
        }
    }
}

섹션도 더 추가할 수 있어요~!!

 

직렬화의 의미는 데이터를 바이트화 시켜서 저장 및 불러오기가 가능하다고 이해하면 되겠습니다.

기존의 바이너리, 스트림을 이용해서 파일을 접근해도 되지만

시리얼라이저블을 이용하여 아주 편리하게 데이터를 쉽게 불러올 수 있습니다.

그 밖에도 Json, Soap 정도가 있겠지만 우선은 까먹기전에 정리해둡니다!

 

(멤버 변수에 위에 [NonSerialized] 선언을 해주면 해당 변수는 제외할 수 있습니다.)

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary; //BinaryFormatter

namespace ConsoleApp2
{
    class Program
    {
        [Serializable] //클래스는 반드시 시리얼라이즈 가능하다고 명시를 해주어야 가능합니다.
        class TestClass
        {
            public int value1;
            public int value2;
            public string value3;
        }

        private const string fileName = "testFile.txt";

        static void Main(string[] args)
        {
            TestClass testClass = new TestClass();

            testClass.value1 = 30;
            testClass.value2 = 50;
            testClass.value3 = "Test";

            string path = Path.Combine(Environment.CurrentDirectory, fileName);

            //저장 준비
            FileStream fileStream = new FileStream(path, FileMode.Create);
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            
            //저장!
            binaryFormatter.Serialize(fileStream, testClass);
           
            fileStream.Close();
        
            //저장 된 폴더를 여는 작업
            //.NET Framework 에서만 동작됩니다. .NET Core에선 권한이 필요해서 안되네요!
            System.Diagnostics.Process.Start(Environment.CurrentDirectory);


            //다시 읽어오는 코드입니다.
            FileStream fs = new FileStream(path, FileMode.Open);
            BinaryFormatter bf = new BinaryFormatter();

            TestClass t = (TestClass)bf.Deserialize(fs); //object 타입으로 반환되므로 캐스팅 해줍니다.

            Console.WriteLine("읽어 온 값 : ");
            Console.WriteLine("value1 : {0}, value2 : {1}, value3 : {2}", t.value1, t.value2, t.value3);

            fs.Close();
        }
    }
}

 

파일을 열어볼 수 있게 txt파일로 저장했습니다.

 

실행 결과

 

https://www.codeproject.com/Articles/58815/C-Image-PictureBox-Rotations

 

C# Image/PictureBox Rotations

How to rotate any image from the center of that image

www.codeproject.com

이 곳의 소스를 참고하였습니다! ㅎㅎ

 

주요 준비물 (button, textBox, label, pictureBox, timer)

 

실행 결과

 

아래는 전체 코드입니다.

timer1_Tick 에 주요 이미지 복사, 회전 코드가 있고 흐름을 살펴보면

 

1. 새로운 비트맵을 만들어 기존 이미지 크기와 맞추어 그릴 준비를 합니다.

2. Graphics로 비트맵을 지정해줍니다.

3. 피벗을 센터로 옮겨줍니다. (처음엔 0, 0입니다.)

4. 회전 후 다시 피벗을 원 위치 시켜줍니다.

5. 새 이미지 등록 후 기존 이미지 리소스 해제합니다.

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

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private Image masterImage;

        private float centerX;
        private float centerY;

        private float currentAngle = 0;
        private float angle = 1;

        private bool timerOn;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            DialogResult result = openFileDialog.ShowDialog();

            if (result == DialogResult.OK)
            {
                string filePath = openFileDialog.FileName;
                masterImage = new Bitmap(filePath);

                //미리 센터 좌표를 구해둡니다.
                centerX = masterImage.Width / 2;
                centerY = masterImage.Height / 2;

                pictureBox1.Image = (Image)masterImage.Clone(); //참조가 아닌 복사를 해줍니다.
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (masterImage == null)
            {
                MessageBox.Show("이미지가 먼저 등록되어야 합니다.");
                return;
            }

            if (timerOn)
            {
                timer1.Stop();
                timerOn = false;
            }
            else
            {
                float newAngle;
                //텍스트 내용이 숫자가 아닐 수도 있으니 조심해줍니다.
                if (float.TryParse(textBox1.Text, out newAngle) == false)
                {
                    newAngle = 1;
                }
                
                angle = newAngle;
                timer1.Start();
                timerOn = true;
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            currentAngle += angle;

            if (currentAngle >= 360)
            {
                currentAngle -= 360;
            }

            label4.Text = string.Format("{0:F2}", currentAngle);

            Bitmap bitmap = new Bitmap(masterImage.Width, masterImage.Height);
            bitmap.SetResolution(masterImage.HorizontalResolution, masterImage.VerticalResolution);

            Graphics graphics = Graphics.FromImage(bitmap);
            graphics.TranslateTransform(centerX, centerY);      //센터로 피벗을 옮겨줍니다.
            graphics.RotateTransform(currentAngle);             //센터를 중심으로 돌려줍니다.
            graphics.TranslateTransform(-centerX, -centerY);    //다시 피벗을 원위치 시켜줍니다.
            graphics.DrawImage(masterImage, PointF.Empty);      //bitmap에 masterImage를 0,0 기준으로 그려줍니다.

            Image oldImage = pictureBox1.Image; //기존 이미지 메모리 해제를 위해 저장
            pictureBox1.Image = bitmap; //새로운 이미지로 대체해줍니다.
            
            if (oldImage != null) //기존 이미지 리소스 해제
            {
                oldImage.Dispose();
            }
        }
    }
}

 

 

C#에서 마우스 매크로 만드는 것을 차례대로 진행 해보겠습니다. (코드는 맨 마지막에 있습니다!)

만들어본 후엔 필요한 기능을 추가하셔서 사용하시면 되겠습니다. (어렵지 않아요~)

 

먼저 Visual Studio에서 Windows Form 만들기를 해줍니다.

전 .NET Framework 를 사용하였는데 .NET Core를 선택하셔도 상관 없습니다.

.NET Framework는 Windows 에서만 사용 가능하고

.NET Core는 다른 운영체제에서도 사용 가능합니다. 점차 .NET Core를 밀고 있는 추세입니다만

전 윈도우로 하니깐 그냥 프레임워크로 가겠슴돠

 

이름은 MouseMacro 입니다. ㄱㄱ

 

다른 기능 다 필요없습니다. 일단 폼 크기를 마음대로 줄여줍니다.

그리고 오른쪽 하단에 있는 속성 창에서 딱 두가지만 세팅해줍니다. (폼 클릭하고도 속성창이 안보이면 F4 누르세요)

FormBorderStyle = FixedToolWindow

Text = Mouse Macro

로 설정해주겠습니다. 훌륭한 UI 세팅으로 여기까지 따라하셨다면 벌써 70% 완성했습니다.

 

이제 F7을 눌러서 (혹은 마우스 오른쪽 클릭-> 코드 보기) 코드 보기로 갑시다.

MouseMacro를 오른쪽 클릭하면 사진처럼 새 항목을 추가할 수 있게 됩니다. 눌러줍시다.

 

클래스를 새로 만들겁니다. 이름은 HookManager로 결정하겠습니다.

Win32 API 를 사용할 것이기 때문에 코드를 복사 붙여넣기 하면 됩니다.

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace MouseMacro
{
    //C:\Windows\System32\user32.dll 참조
    public class HookManager
    {
        #region WindowsAPI_Mouse Event

        [DllImport("user32.dll")]
        static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);

        //Microsoft MouseEvent Document
        //https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event

        const uint MOUSEEVENTF_MOVE = 0x0001;      // 마우스 이동
        const uint MOUSEEVENTF_ABSOLUTE = 0x8000;   // 전역 위치
        const uint MOUSEEVENTF_LEFTDOWN = 0x0002;    // 왼쪽 마우스 버튼 눌림
        const uint MOUSEEVENTF_LEFTUP = 0x0004;      // 왼쪽 마우스 버튼 떼어짐
        const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;    // 오른쪽 마우스 버튼 눌림
        const uint MOUSEEVENTF_RIGHTUP = 0x00010;      // 오른쪽 마우스 버튼 떼어짐
        const uint MOUSEEVENTF_WHEEL = 0x0800;        // 마우스 휠 (dwData 값으로 조정)

        #endregion

        #region WindowsAPI_Key Hook

        //Microsoft Hook Document
        //https://docs.microsoft.com/en-us/windows/win32/winmsg/hooks

        [DllImport("user32.dll")]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc callback, IntPtr hInstance, uint threadId);

        [DllImport("user32.dll")]
        private static extern bool UnhookWindowsHookEx(IntPtr hInstance);

        [DllImport("user32.dll")]
        private static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, int wParam, IntPtr lParam);

        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibrary(string lpFileName);

        private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
        private LowLevelKeyboardProc lowLevelKeyboardProc = HookProc;

        //WM_Keydown
        //https://wiki.winehq.org/List_Of_Windows_Messages
        const int WH_KEYBOARD_LL = 13;
        const int WM_KEYDOWN = 256;
        const int WM_KEYUP = 257;
        const int WM_SYSKEYDOWN = 260; //Alt Key Down

        private static IntPtr hookId = IntPtr.Zero;

        #endregion

        //Custom Variables
        private static Point preMousePosition;

        private static int leftControl = 1;
        private static int controlKey = 0;

        private static uint wheel = 120;
        private static uint backWheel = unchecked((uint)-120);

        public void SetHook()
        {
            IntPtr hInstance = LoadLibrary("User32");
            hookId = SetWindowsHookEx(WH_KEYBOARD_LL, lowLevelKeyboardProc, hInstance, 0);
        }

        public void UnHook()
        {
            UnhookWindowsHookEx(hookId);
        }

        public static IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            if (code >= 0)
            {
                if (wParam == (IntPtr)WM_KEYDOWN)
                {
                    Keys key = (Keys)Marshal.ReadInt32(lParam);

                    switch (key)
                    {
                        case Keys.NumPad0:
                            mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
                            mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
                            return (IntPtr)1;

                        case Keys.NumPad1:
                            Cursor.Position = new Point(preMousePosition.X, preMousePosition.Y);
                            return (IntPtr)1;

                        case Keys.NumPad4:
                            preMousePosition = Cursor.Position;
                            return (IntPtr)1;

                        case Keys.LControlKey:
                            controlKey |= leftControl;
                            break;

                        case Keys.Up:
                            if (controlKey > 0)
                            {
                                mouse_event(MOUSEEVENTF_WHEEL, 0, 0, wheel, 0);
                                return (IntPtr)1;
                            }
                            break;

                        case Keys.Down:
                            if (controlKey > 0)
                            {
                                mouse_event(MOUSEEVENTF_WHEEL, 0, 0, backWheel, 0);
                                return (IntPtr)1;
                            }
                            break;
                    }
                }
                else if (wParam == (IntPtr)WM_KEYUP)
                {
                    Keys key = (Keys)Marshal.ReadInt32(lParam);

                    switch (key)
                    {
                        case Keys.LControlKey:
                            controlKey &= ~leftControl;
                            break;
                    }
                }
            }

            return CallNextHookEx(hookId, code, (int)wParam, lParam);
        }
    }
}

코드가 좀 길지만 다른 곳은 보지 말고 (관심 있다면 살펴봐도 좋습니다.)

public static IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam)

이 곳만 보고 입 맛에 맞게 수정하면 됩니다.

 

로직을 설명해보면

if (code >= 0) // 무언가 키 이벤트가 발생했다.

  if (wParam == (IntPtr)WM_KEYDOWN) //ㅇㅇ 키가 눌렸다고!

    Keys key = (Keys)Marshal.ReadInt32(lParam)

 

      key == Keys.???

      이제 key가 무엇인지에 따라 동작을 해주면 됩니다.

      (참고로 Alt 키는 wParam == WM_SYSKEYDOWN)

      (Label 또는 MessageBox에 출력해서 값을 봐보는 것도 좋습니당)

     

위의 샘플 코드는 numpad4 : 현재 마우스 위치를 저장하고

                       numpad1 : 저장 된 마우스 위치로 마우스 커서 이동

                       numpad0 : 마우스 클릭 발생

 

                       ctrl + 마우스 위/아래 : ctrl + wheel을 구현한 것으로 확대, 축소 기능입니다.

 

자세한 것은 코드를 분석해보시면 되겠습니다. 친절하게 마우스와 키 값에 대한 링크도 해두었습니다!

 

이제 마무리로 HookManager를 인스턴스화 해서 사용하면 됩니다.

이벤트 두 개만 추가해봅시다. 다시 폼 뷰로 돌아와서 속성 창의 번개 모양을 클릭해줍니다.

그러면 이벤트를 등록할 수 있게 됩니다.

Load를 더블 클릭하면 코드가 자동으로 생성되는데 다시 폼뷰로 돌아와

FormClosing도 더블 클릭해줍니다.

 

이제 마지막 코드입니다. 간단해유

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

namespace MouseMacro
{
    public partial class Form1 : Form
    {
        private HookManager hookManager;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //화면 가장 오른쪽에 띄우도록 합니다.
            int x = Screen.PrimaryScreen.Bounds.Width - this.Size.Width;
            int y = 0;

            this.Location = new Point(x, y);

            hookManager = new HookManager();
            hookManager.SetHook();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            hookManager.UnHook();
        }
    }
}

로드 될 때 폼을 오른쪽 상단에 배치하고 훅을 시작합니다.

폼이 꺼질 때 훅을 해제 해주며 마무리 합니다.

여기까지 다 만드셨다면 간단한 마우스 매크로 성공입니다! 실행해서 확인해 보시고..

 

추가 기능은 HookManager의 HookProc 를 수정해서 사용해보세요~

+ Recent posts