m.cafe.daum.net/smbitpro/R7EG/11
컨트롤 개요 (2.이벤트 처리 )
2. 이벤트 처리
컨트롤은 사용자와 상호 작용을 할 수 있는 구성요소이다.
사용자의 컨트롤 제어나 컨트롤의 상태 변화에 따라 적절한 이벤트가 발생하고 발생한 이벤트에 대한 처리를 하는 메서드를 이벤트 핸들러라 한다.
여기에서는 회원 관리 프로그램을 통해 컨트롤의 이벤트 처리에 대해 느낄 수 있도록 할 것이다.
2.1 새로운 폼 띄우기
앞에서 설명했듯이 우리가 작성할 회원 관리 프로그램은 MainForm과 SettingForm, InputForm이 있다.
MainForm의 메인 메뉴è 기능 è 설정을 통해 SettingForm을 띄워지고 메인 메뉴è 기능 è 추가를 통해 InputForm을 띄우는 것을 해 보도록 하자.
MainForm의 디자인 창에서 메뉴를 선택하고 설정 메뉴 아이템을 더블 클릭을 하면 Click이벤트 핸들러가 자동 설정된다. 또 다른 방법으로 속성 창의 이벤트 탭을 이용하여 Click 이벤트 부분을 더블 클릭하더라도 동일하다. 혹은 소스 코드에서 직접 작성을 할 수도 있을 것이다.
다음, 자동으로 추가되는 코드이다.
MainForm.Designer.cs의 private void InitializeComponent() 내부에 추가되는 코드
this.SettingToolStripMenuItem.Click += new System.EventHandler(this.SettingToolStripMenuItem_Click); |
MainForm.cs의 MainForm Class내부에 추가되는 코드
private void SettingToolStripMenuItem_Click(object sender, EventArgs e) {
} |
우리는 새로 생성된 Click 이벤트 핸들러인 SettingToolStripMenuItem()메서드 내부 코드를 목적에 맞게 작성을 하면 된다.
우리가 작성할 회원 관리 프로그램에서 설정 폼은 최대 하나의 폼만 뜨도록 하자. 이를 위해서 GlobalData 클래스에서 이에 대해 관리를 하도록 하자.
설정 메뉴의 Click 이벤트 핸들러인 SettingToolStripMenuItem() 메서드에서는 GlobalData 클래스의 static 메소드를 통해 SettingForm을 띄워 달라고 요청을 하고 실제 처리는 GlobalData에서 하면 될 것이다.
다음은 SettingToolStripMenuItem() 메서드의 소스 코드이다.
private void SettingToolStripMenuItem_Click(object sender, EventArgs e) { GlobalData.ShowSettingForm(); } |
다음은 GlobalData.cs의 GlobalData 클래스 내부 코드이다.
static SettingForm settingForm = null; internal static void ShowSettingForm() { if (settingForm == null) { settingForm = new SettingForm(); settingForm.Show(); } } |
동일한 방법으로 추가 메뉴 아이템을 통해 InputForm을 띄울 수 있도록 하자.
2.2 설정
회원 관리 프로그램의 MainForm은 리스트 뷰로 회원 목록을 보여주는데 설정을 통해 보여주는 회원 속성 목록을 변하게 해 보자.
설정에 관련된 구성 요소로는 사용자가 설정을 하기 위한 폼으로는SettingForm과 설정에 따라 회원 목록을 보여주는 MainForm이 있다. 그리고, 설정된 내용을 보관하고 파일로 저장하거나 파일로부터 설정을 로딩하는 Setting클래스와 Settting 클래스 기반의 인스턴스를 관리 및 제어하는 GlobalData가 있다.
2.2.1 MainForm의 Load 이벤트 발생 시 시퀀스
MainForm이 Load될 때 ListView의 컬럼 컬렉션을 설정을 해야 한다.
설정을 위해서는 각 회원 속성을 보여줄 것인지 여부에 대해 얻어와야 한다.
각 회원 속성을 보여줄 것인지 여부는 Setting 클래스의 단일 개체인 Singleton을 통해 얻어오게 하자. Setting클래스에 처음 접근을 할 때 static 생성자를 통해 설정 초기화를 하게 된다.
설정 초기화는 설정 파일이 존재하면 이를 통해 설정 값을 얻어와서 Setting 개체를 만들고 존재하지 않으면 디폴트 값으로 Setting 개체를 만드는 것이다.
해당 개체를 통해 어떠한 속성을 보여줄 것인지를 얻어오면 설정을 얻어오는 작업은 완료 된다.
이후 이를 통해 보여주어야 하는 속성들에 해당하는 컬럼 헤더를 생성하여 ListView에 추가하면 된다.
참고로 ListViewSetting은 설정이 변경될 경우에도 수행된다.
이런 이유로 ListViewSetting 메서드에는 헤더를 설정하는 MakeListViewHeader메서드 호출 후에 회원 데이터 리스트를 얻어와서 ListView에 추가하는 ListViewDataSetting메서드 호출이 있는 것이다.
다음은 시퀀스 다이어그램에 해당하는 소스이다.
l MainForm의 Load이벤트 핸들러(MainForm.cs)
MainForm이 Load이벤트 핸들러에서는 ListView를 설정하고 설정이 바뀔 때의 이벤트 핸들러와
멤버가 추가될 때의 이벤트 핸들러를 설정한다.
private void MainForm_Load(object sender, EventArgs e) { ListViewSetting(); GlobalData.ChangeSettingEventHandler += new GlobalData.ChangeSetting(GlobalData_ChangeSettingEventHandler); GlobalData.AddMemberEventHandler += new GlobalData.AddMemberDele(GlobalData_AddMemberEventHandler); } |
l 설정 변경 및 회원 추가 관련 delegate와 이벤트 (GlobalData.cs)
SettingForm을 통해 설정이 변경되었을 때 MainForm은 변경된 설정으로 ListView를 재구성해야 한다. 또한, InputForm을 통해 회원이 추가되었을 때에도 ListView를 재구성해야 한다.
이를 위해 SettingForm이나 InputForm에서 설정이 변경되거나 회원이 추가되면 GlobalData의 멤버 메서드를 호출하며 MainForm에서 설정한 이벤트 핸들러를 GlobalData.cs에는 호출함으로써 이에 대한 처리를 수행할 수 있다.
public delegate void ChangeSetting(); public delegate void AddMemberDele(Member member); public static event ChangeSetting ChangeSettingEventHandler; public static event AddMemberDele AddMemberEventHandler; |
l SetSettingValue(GlobalData.cs)
다음은 설정이 변경되었을 때 SettingForm에서 호출하는 메서드인 SetSettingValue이다. 이 메서드에서는 설정을 변경하고 ChangeSettingEventHandler(설정 변경 이벤트 핸들러)가 설정되어 있다면 이를 호출하도록 구현되어 있다.
internal static void SetSettingValue(int index, bool check) { setting.ChanageSetting(index, check); [안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xxOnChangeSetting(); } public static void [안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xx[안내]태그제한으로등록되지않습니다-xxOnChangeSetting() { if (ChangeSettingEventHandler != null) { ChangeSettingEventHandler(); } } |
l ChageSetting (Setting.cs)
internal void ChanageSetting(int index, bool check) { switch (index) { case Setting.NAME: Name = check; break; case Setting.AGE: Age = check; break; case Setting.GENDER: Gender = check; break; case Setting.MARRIED: Married = check; break; case Setting.HASCAR: Hascar = check; break; default: NotAvail(); break; } } |
l AddMember (GlobalData.cs)
InputForm에서 회원 추가를 누르면 GlobalData에 AddMember메서드가 호출이 되어 회원들을 보관하는 리스트인 members에 해당 회원을 추가하고 AddMemberEventHandler(회원 추가 이벤트 핸들러)가 설정되어 있다면 이를 호출하도록 구현되어 있다.
static List<Member> members = new List<Member>(); public static void OnAddMember(Member member) { if (AddMemberEventHandler != null) { AddMemberEventHandler(member); } } internal static void AddMember(Member member) { members.Add(member); OnAddMember(member); } |
l GlobalData_ChangeSettingEventHandler(MainForm.cs)
설정이 변경되었을 때의 이벤트 핸들러에서는 ListView 설정을 변경하면 된다.
void GlobalData_ChangeSettingEventHandler() { ListViewSetting(); } |
l ListViewSetting(MainForm.cs)
ListView의 설정 변경은 현재 모든 아이템들을 제거하고(Clear) ListView의 보여줄 컬럼을 설정한 후(ListViewHeaderSetting) 회원 리스트를 설정(ListViewDataSetting)하면 된다.
private void ListViewSetting() { listView1.Clear(); ListViewHeaderSetting(); ListViewDataSetting(); } |
l ListHeaderSetting(MainForm.cs)
ListView의 보여줄 컬럼을 설정하기 위해서는 먼저 설정 값들을 얻어오고(GlobalData.GetSettings) 컬럼 아이템들을 제거한 후(Columns.Clear) 설정 헤더들을 만들면 된다.(MakeListViewHeader)
private void ListViewHeaderSetting() { bool[] settingValues = GlobalData.GetSettings(); listView1.Columns.Clear(); for (int i = 0; i < settingValues.Length; i++) { MakeListViewHeader(i, settingValues[i]); } } |
설정 초기화를 위해 Setting의 static 생성자를 통해 Singleton개체를 만드는 부분이 있어 이에 대한 설명을 먼저 하고 위의 ListHeaderSetting메서드에서 호출하는 GetSettings와 MakeListViewHeader에 대한 설명을 할 것이다.
l Setting 초기화 - 클래스의 Singleton 개체 생성 (GlobalData.cs)
Setting의 Singleton개체를 GlobalData에서 참조하고 있다.
static Setting setting = Setting.Singleton; |
l Setting 클래스의 static 생성자 – 설정 초기화(Setting.cs)
Setting의 static 생성자에서는 설정의 헤더 이름을 초기화(SettingInitialize)하고 설정을 Load(SettingLoad)하는 메서드 호출로 이루어진다.
static public Setting Singleton; static Setting() { SettingInitialize(); SettingLoad(); } |
l SettingInitilalize(Setting.cs)
설정 초기화에서는 설정 헤더 string을 대입한다.
private static void SettingInitialize() { indexNames = new string[Setting.MAX]; indexNames[Setting.NAME]= "이름"; indexNames[Setting.AGE] = "나이"; indexNames[Setting.GENDER] = "성별"; indexNames[Setting.MARRIED] = "결혼 유무"; indexNames[Setting.HASCAR] = "자동차 소유 유무"; } |
l Setting 클래스의 상수(Setting.cs)
public const int NAME = 0; public const int AGE = 1; public const int GENDER = 2; public const int MARRIED = 3; public const int HASCAR = 4; public const int MAX = 5; |
l SettingLoad(Setting.cs)
설정을 Load하는 SettingLoad에서는 설정 파일이 존재하며 이를 통해 Singleton개체를 역직렬화 (SingletonByDeserialize)하고 그렇지 않으면 디폴트 설정 파일을 만든다(MakeDefaultSettingFile).
const string settingfname = "setting.dat"; private static void SettingLoad() { if (File.Exists(settingfname)) { SigletonByDeserialize(); } else { MakeDefaultSettingFile(); } } |
l SingletonByDeserialize(Setting.cs)
설정 파일을 열고 BinaryFomatter로 역직렬화 하여 Singleton에 대입한다. 이를 위해 Setting클래스는 [Serializable]해야 한다.
private static void SigletonByDeserialize() { FileStream fs = new FileStream(settingfname, FileMode.Open, FileAccess.Read); BinaryFormatter bf = new BinaryFormatter(); Singleton = bf.Deserialize(fs) as Setting; fs.Close(); } |
l MakeDefaultFileSetting(Setting.cs)
디폴트 설정 파일을 만드는 MakeDefaultSettingFile에서는 디폴트 값으로 Setting개체를 생성하여 Singleton에 대입하고 이를 저장하는 코드로 이루어진다.
private static void MakeDefaultSettingFile() { Singleton = new Setting(true, true, true, true, true); Singleton.SaveSetting(); } |
l Setting 생성자(Setting.cs)
Setting 생성자는 Setting의 각 속성을 입력 인자로 대입한다.
private Setting(bool name, bool age, bool gender, bool married, bool hascar) { Name = name; Age = age; Gender = gender; Married = married; Hascar = hascar; } |
l Setting 멤버 속성(Setting.cs)
Setting의 멤버 속성은 외부에서 get할 수 있지만 설정은 내부에서만 가능하도록 하였다.
public bool Name { get; private set; } public bool Age { get; private set; } public bool Gender { get; private set; } public bool Married { get; private set; } public bool Hascar { get; private set; } |
l SaveSetting(Setting.cs)
설정을 저장하는 것은 설정파일을 열고 BinaryFormatter를 통해 직렬화하면 된다.
public void SaveSetting() { FileStream fs = new FileStream(settingfname, FileMode.Create); BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(fs, this); fs.Close(); } |
여기서 부터는 앞의 ListHeaderSetting메서드에서 호출했던 GetSettings와 MakeListViewHeader에 대한 설명이다.
l GlobalData.GetSettings(GlobalData.cs)
설정의 정보를 얻어오는 GetSettings메서드는 Setting클래스의 Singleton개체를 참조하는 setting을 통해 각 속성의 설정 값을 얻어온 것을 반환하는 작업으로 이루어진다.
static Setting setting = Setting.Singleton; internal static bool[] GetSettings() { settingValues[Setting.NAME] = setting.Name; settingValues[Setting.AGE] = setting.Age; settingValues[Setting.GENDER] = setting.Gender; settingValues[Setting.MARRIED] = setting.Married; settingValues[Setting.HASCAR] = setting.Hascar; return settingValues; } |
l MakeListViewHeader (MainForm.cs)
MakeListViewHeader에서는 설정할 속성의 인덱스와 실제 보여줄 지 여부를 입력 매개변수로 받아 보여주어야 하는 속성이라면 컬럼 헤더를 생성하여 설정의 해당 인덱스의 텍스트를 얻어와 컬럼 헤더의 Text에 대입하고 리스트 뷰의 컬럼 컬렉션에 추가한다.
private void MakeListViewHeader(int index, bool visible) { if (visible) { ColumnHeader columnHeader = new ColumnHeader(); columnHeader.Text = Setting.GetSettingIndexText(index); columnHeader.Tag = index; listView1.Columns.Add(columnHeader); } } |
l Setting.GetSettingIndexText (Setting.cs)
설정의 특정 인덱스에 해당하는 string을 얻어온다. 이는 리스트 뷰의 컬럼 헤더에 사용될 문자열이다.
static string[] indexNames; public static String GetSettingIndexText(int index) { return indexNames[index]; } |
l ListViewDataSetting (MainForm.cs)
ListViewDataSetting에서는 전체 회원수를 얻어오고(GlobalData.GetMemberCount) 루프를 통해 각 회원 정보를 얻어온(GlobalData.GetMember) 후 회원 추가 이벤트 핸들러를 호출하여 전체 회원 목록을 리스트 뷰에 추가한다.
private void ListViewDataSetting() { int index = GlobalData.GetMemberCount(); Member member = null; for (int i = 0; i < index; i++) { member = GlobalData.GetMember(i); GlobalData_AddMemberEventHandler(member); } } |
l GlobalData.GetMemeberCount (GlobalData.cs)
전체 회원수는 회원을 보관하는 리스트인 members의 멤버 Count와 동일하다.
public static int GetMemberCount() { return members.Count; } |
l GlobalData.GetMember (GlobalData.cs)
Index번째의 회원은 회원을 보관하는 리스트의 index의 멤버를 반환하면 된다.
public static Member GetMember(int index) { return members[index]; } |
l GlobalData_AddMemberEventHandler(MainForm.cs)
회원을 추가하는 이벤트 핸들러에서는 먼저 설정값을 얻어온(GlobalData.GetSettings) 후 리스트 뷰의 컬럼 헤더만큼 루프를 돌면서 리스트 뷰의 아이템을 만들어 리스트 뷰에 추가하면 된다.
처음 루프에 온 것이면 리스트 뷰 아이템을 생성해야 한다.
void GlobalData_AddMemberEventHandler(Member member) { bool[] settings = GlobalData.GetSettings(); ListViewItem lvi = null; bool first = true; foreach (ColumnHeader columnHeader in listView1.Columns) { if (first) { lvi = new ListViewItem(MakeSubItem(columnHeader, member)); first = false; } else { string data = MakeSubItem(columnHeader, member); if (lvi != null) { lvi.SubItems.Add(data); } } } if (lvi != null) { listView1.Items.Add(lvi); } } |
l MakeSubItem (MainForm.cs)
리스트 뷰의 아이템의 서브 아이템을 만드는 작업에서는 컬럼 헤더의 Text값에 따라 회원의 해당 멤버의 string변환값을 반환하는 것이다.
private string MakeSubItem(ColumnHeader columnHeader, Member member) { switch (columnHeader.Text) { case "이름": return member.Name; case "나이": return member.Age.ToString(); case "결혼 유무": return member.Gender.ToString(); case "성별": return member.Married.ToString(); case "자동차 소유 유무": return member.Hascar.ToString(); } return string.Empty; } |
l Member 클래스 (Member.cs)
멤버 클래스는 다음과 같다.
[Serializable] class Member { #region 멤버 속성 public string Name { get; private set; } public int Age { get; private set; } public bool Gender { get; private set; } public bool Married { get; private set; } public bool Hascar { get; private set; } #endregion public Member(string name, int age, bool gender, bool married, bool hascar) { Name = name; Age = age; Gender = gender; Married = married; Hascar = hascar; } } |
2.2.2 SettingForm의 Load 이벤트 발생 시 시퀀스
SettingForm이 Load될 때 설정 데이터를 얻어와서 SettingBox(MainForm의 리스트 뷰의 보여줄 항목을 설정하기 위한 CheckedListBox)를 설정하는 작업을 해야 한다.
다음 그림은 SettingForm Load시 수행해야 하는 시퀀스 다이어그램이다.
시퀀스 다이어그램에 해당하는 소스 코드는 다음과 같다.
l SettingForm_Load (SettingForm.cs)
SettingBox(MainForm의 리스트 뷰의 보여줄 항목을 설정하기 위한 CheckedListBox)를 설정하고 SettingBox의 항목의 체크 상태를 설정한다.
private void SettingForm_Load(object sender, EventArgs e) { SettingBoxItemSetting(); SettingBoxItemStateSetting(); } |
l SettingBoxItemSetting (SettingForm.cs)
설정 항목의 최대값을 얻어오고(Setting.MAX) 각 항목의 Text를 얻어온 (GetSettingIndexText) 값으로SettingBox의 항목을 구성한다.
private void SettingBoxItemSetting() { int maxitems = Setting.MAX; for (int i = 0; i < maxitems; i++) { SettingBox.Items.Add(Setting.GetSettingIndexText(i)); } } |
l SettingBoxItemStateSetting (SettingForm.cs)
설정 값을 얻어오고(GlobaData.GetSettings) 얻어온 값으로 각 항목의 상태 값을 설정한다.
private void SettingBoxItemStateSetting() { bool[] settingValues = GlobalData.GetSettings(); int i = 0;
for (i = 0; i < settingValues.Length; i++) { SettingBox.SetItemChecked(i, settingValues[i]); } } |
2.2.3 SettingForm의 SettingBox 컨트롤의 SelectedIndexChanged 이벤트 발생 시 SettingForm의 SettingBox내의 항목의 상태가 변경이 되면 SelectedIndexChanged 이벤트가 발생이 된다. 이 프로그램에서는 SettingBox의 상태 변경에 따라 Setting개체의 속성이 변경이 되고 이 사항을 반영하여 MainForm의 보여주는 Member 항목이 변해야 한다.
MainForm에서는 상태가 변경이 될 때 수행할 이벤트 핸들러를 정의하여 GlobalData의 ChageSettingEventHandler에 설정을 함으로써 설정이 변경될 때 원하는 동작을 할 수 있음을 2.2.1에서 설명한 바 있다.
다음은 이에 대한 시퀀스 다이어그램이다.
다음이 시퀀스 다이어그램에 해당하는 소스 코드이다.
l checkedListBox1_SelectedIndexChanged (SettingForm.cs)
이 메서드는 SettingForm의 SettingBox 컨트롤의 항목이 바뀔 때 발생하는 이벤트 핸들러이다. 여기에서는 선택된 항목의 인덱스를 얻어온 후 해당 항목의 설정값을 얻어와 설정 값을 변경하면 된다.
private void checkedListBox1_SelectedIndexChanged(object sender, EventArgs e) { int index = SettingBox.SelectedIndex; bool check = SettingBox.GetItemChecked(index); GlobalData.SetSettingValue(index,check); } |
l checkedListBox1_SelectedIndexChanged (SettingForm.cs)
이 메서드는 SettingForm의 SettingBox 컨트롤의 항목이 바뀔 때 발생하는 이벤트 핸들러이다. 여기에서는 선택된 항목의 인덱스를 얻어온 후 해당 항목의 설정값을 얻어와 설정 값을 변경하면 된다. GlobalData.SettingValue메서드에 대해서는 2.2.1에서 다루었다.
2.2.4 SettingForm의 FormClosed 이벤트 발생 시
SettingForm이 닫히게 되면 이를 GlobalData에 통보하여 주어야 한다. 이는 MainForm의 설정 메뉴 아이템을 선택하였을 때 이미 SettingForm이 띄워져 있지 않은 경우에만 띄워야 하는데 이를 판별하기 위해서 GlobalData에는 SettingForm 형식의 멤버 필드인 settingForm이 있다.
2.1을 참고하라.
l SettingForm_FormCloased(SettingForm.cs)
GlobalData에 이를 통보해 주어야 한다.
private void SettingForm_FormClosed(object sender, FormClosedEventArgs e) { GlobalData.SettingFormClosed(); } |
l SettingFormClosed(GlobalData.cs)
settingForm을 null로 대입하여 띄워져 있는 설정 창이 없다는 것을 설정해야 한다.
internal static void SettingFormClosed() { settingForm = null; } |
2.3 추가
MainForm의 추가 메뉴 아이템을 선택하면 InputForm이 뜨고 이를 통해 회원이 추가된다.
2.3.1 InputForm의 회원 추가 버튼을 눌렀을 때
InputForm의 회원 추가 버튼은 buttonOfConfirm이라 명명하였다. 이 버튼을 누르면 Click이벤트가 발생한다.
회원 추가 버튼의 Click이벤트가 발생하면 회원 정보를 입력하는 각 컨트롤의 상태 값을 얻어와서 Member 개체를 생성하고 생성된 회원을 추가하면 된다. 그리고, 회원 정보를 입력하는 각 컨트롤의 상태 값을 초기화 하면 작업이 완료된다.
l buttonOfConfirm_Click(InputForm.cs)
private void buttonOfConfirm_Click(object sender, EventArgs e) { string name = textBoxOfName.Text; int age = (int)numericUpDownOfAge.Value; bool gender = radioButtonFemale.Checked; bool married = checkBoxOfMarried.Checked; bool hascar = checkBoxOfCar.Checked; Member member = new Member(name, age, gender, married, hascar); GlobalData.AddMember(member); ClearData(); } |
l ClearData(InputForm.cs)
private void ClearData() { textBoxOfName.Text = string.Empty; numericUpDownOfAge.Value = 0; radioButtonFemale.Checked = true; checkBoxOfMarried.Checked = false; checkBoxOfCar.Checked = false; } |
2.3.2 InputForm의 취소 버튼을 눌렀을 때
InputForm의 취소 버튼은 buttonOfCancel이라 명명하였다. 이 버튼을 누르면 Click이벤트가 발생한다.
취소 버튼의 Click이벤트가 발생하면 회원 정보를 입력하는 각 컨트롤의 상태 값을 초기화 하면 된다.
l buttonOfCancel_Click(InputForm.cs)
private void buttonOfCancel_Click(object sender, EventArgs e) { ClearData(); } |
2.3.3 InputForm의 FormClosed 이벤트 발생했을 때
InputForm이 닫혔을 때의 처리는 SettingForm이 닫혔을 때와 동일한 형태로 처리하면 된다. 2.2.4를 참고하라.
l InputForm_FormClosed(InputForm.cs)
private void InputForm_FormClosed(object sender, FormClosedEventArgs e) { GlobalData.InputFormClosed(); } |
l InputFormClosed(GlobalData.cs)
internal static void InputFormClosed() { inputForm = null; } |
'개발언어 > C#' 카테고리의 다른 글
[C#] 윈폼 dpi와 width,height가 안맞음 (0) | 2021.04.12 |
---|---|
[C#] (강의) 5. 사용자 정의 컨트롤 (0) | 2021.04.12 |
[C#] (강의) 4. 컨트롤 개요 (폼 배치) (0) | 2021.04.12 |
[C#] (강의) 3. Form 이벤트 (마우스/작업) (0) | 2021.04.12 |
[C#] (강의) 3. Form 이벤트 (키) (0) | 2021.04.12 |