전편을 안보신분은 #1을 먼저 보실것을 추천드립니다.

 

C#으로 JIRA API REST 제어/연동하기 #1

프로젝트를 진행하다 보면 JIRA를 사용하는 경우가 많다. 흔히 우리가 알고있는 아틀라시안 JIRA를 내가 있는 현장에서도 많이 사용하고 있었다. Atlassian | 소프트웨어 개발 및 협업 도구 전 세계

racer42.tistory.com

#2에서는 메인화면의 구성과 내용에 대해서 설명하겠습니다.

필요했던 기능은 크게 두가지입니다.
  1. JIRA 티켓생성기능(Create Ticket)
  2. JRIA 티켓검색기능(Search Ticket)

JIRA 티켓생성기능(Create Ticket)
이 티켓을 어떻게 만들어야할까...
고민하면서 여러가지 자료를 검토해 보았습니다.
우선 먼저 만들어두신 분들이 있기에 JAVA 관련 자료는 아래를 참고하시면 될것 같습니다.

 

Jira Software Rest API 이용한 이슈 등록

"Jira Rest API 이용한 이슈 등록 "● REST API 기존에 사용하는 서비스에서 이슈를...

blog.naver.com

저는 C#으로 만들예정이므로 아래의 페이지를 참고하였습니다.

 

.NET Framework: 780. C# - JIRA REST API 사용 정리

.NET Framework: 780. C# - JIRA REST API 사용 정리 [링크 복사], [링크+제목 복사] 조회: 6293 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 [jira_rest_api_sample.zip]     부모글 보이기/감추기 C# - J

www.sysnet.pe.kr

JIRA REST API 의 샘플은 공식문서를 참고하였습니다.

 

Jira REST API examples

Jira REST API examples This guide contains different examples of how to use the Jira REST API, including how to query issues, create an issue, edit an issue, and others. The reference documentation for the Jira Server platform REST API is here:  Jira Serv

developer.atlassian.com

우선, 사용되는 클래스 정의는 아래와 같습니다.

using System;
using System.Collections.Generic;

public class Progress
{
    public int progress { get; set; }
    public int total { get; set; }
}

public class Issuetype
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public bool subtask { get; set; }
}

public class Votes
{
    public string self { get; set; }
    public int votes { get; set; }
    public bool hasVoted { get; set; }
}

public class Resolution
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string name { get; set; }
}

public class AvatarUrls
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Reporter
{
    public string self { get; set; }
    public string name { get; set; }
    public string emailAddress { get; set; }
    public AvatarUrls avatarUrls { get; set; }
    public string displayName { get; set; }
    public bool active { get; set; }
}

public class Priority
{
    public string self { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class Watches
{
    public string self { get; set; }
    public int watchCount { get; set; }
    public bool isWatching { get; set; }
}

public class Status
{
    public string self { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class AvatarUrls2
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Assignee
{
    public string self { get; set; }
    public string name { get; set; }
    public string emailAddress { get; set; }
    public AvatarUrls2 avatarUrls { get; set; }
    public string displayName { get; set; }
    public bool active { get; set; }
}

public class AvatarUrls3
{
    public string __invalid_name__16x16 { get; set; }
    public string __invalid_name__24x24 { get; set; }
    public string __invalid_name__32x32 { get; set; }
    public string __invalid_name__48x48 { get; set; }
}

public class Project
{
    public string self { get; set; }
    public string id { get; set; }
    public string key { get; set; }
    public string name { get; set; }
    public AvatarUrls3 avatarUrls { get; set; }
}

public class Customfield10300
{
    public string self { get; set; }
    public string value { get; set; }
    public string id { get; set; }
}

public class Aggregateprogress
{
    public int progress { get; set; }
    public int total { get; set; }
}

public class Status2
{
    public string self { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public string id { get; set; }
}

public class Issuetype2
{
    public string self { get; set; }
    public string id { get; set; }
    public string description { get; set; }
    public string iconUrl { get; set; }
    public string name { get; set; }
    public bool subtask { get; set; }
}

public class Fields2
{
    public string summary { get; set; }
    public Status2 status { get; set; }
    public Issuetype2 issuetype { get; set; }
}

public class Parent
{
    public string id { get; set; }
    public string key { get; set; }
    public string self { get; set; }
    public Fields2 fields { get; set; }
}

public class Fields
{
    public string summary { get; set; }
    public Progress progress { get; set; }
    public Issuetype issuetype { get; set; }
    public Votes votes { get; set; }
    public List<object> fixVersions { get; set; }
    public Resolution resolution { get; set; }
    public DateTime? resolutiondate { get; set; }
    public object timespent { get; set; }
    public Reporter reporter { get; set; }
    public object aggregatetimeoriginalestimate { get; set; }
    public DateTime updated { get; set; }
    public DateTime created { get; set; }
    public string description { get; set; }
    public Priority priority { get; set; }
    public string duedate { get; set; }
    public List<object> issuelinks { get; set; }
    public Watches watches { get; set; }
    public object customfield_10600 { get; set; }
    public List<object> subtasks { get; set; }
    public Status status { get; set; }
    public List<object> labels { get; set; }
    public long workratio { get; set; }
    public Assignee assignee { get; set; }
    public string customfield_10500 { get; set; }
    public object aggregatetimeestimate { get; set; }
    public Project project { get; set; }
    public List<object> versions { get; set; }
    public string environment { get; set; }
    public object timeestimate { get; set; }
    public Customfield10300 customfield_10300 { get; set; }
    public Aggregateprogress aggregateprogress { get; set; }
    public DateTime? lastViewed { get; set; }
    public object timeoriginalestimate { get; set; }
    public object aggregatetimespent { get; set; }
    public Parent parent { get; set; }
}

public class Issue
{
    public string expand { get; set; }
    public string id { get; set; }
    public string self { get; set; }
    public string key { get; set; }
    public Fields fields { get; set; }
}

public class SearchResult
{
    public string expand { get; set; }
    public int startAt { get; set; }
    public int maxResults { get; set; }
    public int total { get; set; }
    public List<Issue> issues { get; set; }
}

 

먼저 검색기능을 구현해 보았습니다.

        /// <summary>
        /// 作業中
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void btnSearch_Click(object sender, EventArgs e)
        {
            // Grid初期化
            dgDoing.Rows.Clear();

            // 件数カウンター
            lblCount1.Text = 0.ToString();

            // ①.JIRA認証を行う
            Jira jira = new Jira();
            if (await jira.Login(jiraServer, id, password) == false)
            {
                Console.WriteLine("Auth failed: " + id);
                return;
            }

            // ②.アサインを整理する
            assignee = SetAssignMember();

            // ③.fixversionを整理する
            fixversion = SetFixVersion();

            // ③.検索を実施する
            SearchResult resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), 0, true);

            // チケット設定
            ListDoing = resultDoing.issues;

            // ◆◆◆◆◆ DOINGリスト ◆◆◆◆◆
            lblCount1.Text = 0.ToString();
            if (ListDoing != null)
            {
                ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));
                ListDoing = resultDoing.issues;
                int count = 1;

                foreach (var issue in ListDoing)
                {
                    dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                }

                if (ListDoing.Count == 51)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 101)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 151)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                // 件数カウンター
                lblCount1.Text = (count - 1).ToString();
            }
        }

여기서 왜 ??
아래처럼 건수별 분기를 하였는지 설명드리자면, json 형식으로 데이터를 수신하면서 최대 수신 문자열을 초과하는 상황이 발생하므로 카운팅된 건수 다음을 다시 가져오게끔 하기위해서 두번째 세번째 요구를 보내고 있습니다.
지금은 200건까지 데이터를 가져오도록 하였는데 더 확장하는것도 가능합니다.

                foreach (var issue in ListDoing)
                {
                    dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                }

                if (ListDoing.Count == 51)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 101)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

                if (ListDoing.Count == 151)
                {
                    resultDoing = await jira.GetIssuesByAssignee(txtDate.Text, fixversion, assignee, setLabels(), count, true);
                    ListDoing = resultDoing.issues;
                    ListDoing.Sort((a, b) => string.Compare(a.fields.assignee.displayName, b.fields.assignee.displayName));

                    count = 51;
                    foreach (var issue in ListDoing)
                    {
                        dgDoing.Rows.Add(count++, issue.fields.assignee.displayName, jiraUri + issue.key, issue.fields.summary, issue.fields.status.name, issue.fields.updated, issue.fields.duedate);
                    }
                }

실제 검색을 날리는 부분을 살펴보면
등장하는 jql 은 
duedate : 기간
assignee : 담당
fixversion : 픽스버젼
labels : 라벨
project = 프로젝트명 <- 삭제하였음
등이 있습니다.
jql 의 사용법에 대해서는 developer.atlassian.com/server/jira/platform/jira-rest-api-examples/ 페이지를 참고하세요.

        /// <summary>
        /// 複合的な自由検索
        /// </summary>
        /// <param name="date"></param>
        /// <param name="fixversion"></param>
        /// <param name="assignee"></param>
        /// <returns></returns>
        public async Task<SearchResult> GetIssuesByAssignee(string date, string fixversion, string assignee, string labels, int startAt, bool doneJudge)
        {
            string url = _baseUrl + "search?jql=duedate < " + date + "d and assignee in (" + assignee + ")";

            string url2 = 0.Equals(startAt) ?
                string.Empty : " and startAt = " + startAt.ToString();

            url2 = !string.IsNullOrEmpty(fixversion) ?
                " and fixVersion in (" + fixversion + ")" : string.Empty;

            if (!string.IsNullOrEmpty(labels))
            {
                url2 += " and labels in (" + labels + ")";
            }

            string url3 = " and status not in (Closed,Resolved)";
            string url4 = " and status in (Closed,Resolved)";

            HttpResponseMessage hrm = doneJudge ? await _httpClient.GetAsync(url + url2 + url3) : await _httpClient.GetAsync(url + url2 + url4);

            //string text = await hrm.Content.ReadAsStringAsync();
            SearchResult result = Newtonsoft.Json.JsonConvert.DeserializeObject<SearchResult>(await hrm.Content.ReadAsStringAsync());
            return result;
        }

이것을 

Newtonsoft.Json.JsonConvert.DeserializeObject<SearchResult>(await hrm.Content.ReadAsStringAsync());

을 통해서 요청하고 검색결과를 SearchResult 클래스 형태로 담아냅니다.

Done검색에서는 Jql만 조금 바뀐형태인데, Status 의 쿼리를 약간 변형한 형태입니다.

await _httpClient.GetAsync(url + url2 + url4)

url / url2 / url4 를 합쳐서 쿼리를 날리고있습니다.

url3 과 url4 의 차이점

여기까지 완료되면, 아래처럼 검색결과가 나오는것을 보실수 있습니다.

검색결과

데이터 그리드의 링크는 클릭하시면, 지정된 브라우져를 통해서 새창으로 보실수있습니다.

 

DataGridView 클래스 (System.Windows.Forms)

사용자 지정 가능한 표에 데이터를 표시합니다.Displays data in a customizable grid.

docs.microsoft.com

#3에서 등록에 대해서 다루어 보겠습니다.
현재 다루고있는 소스의 전체코드는 아래에 있습니다.

 

 

sungmanko/JiraRestAPI

This is a JIRA ticket automatic creation and management project created in C#. - sungmanko/JiraRestAPI

github.com

 

프로젝트를 진행하다 보면 JIRA를 사용하는 경우가 많다.
흔히 우리가 알고있는 아틀라시안 JIRA를 내가 있는 현장에서도 많이 사용하고 있었다.

 

Atlassian | 소프트웨어 개발 및 협업 도구

전 세계 수백 만 사용자가 Atlassian 제품을 이용해 소프트웨어 개발과 프로젝트 관리, 협업, 코드 품질을 개선하고 있습니다.

www.atlassian.com

이번 주제는 이 JIRA를 많이 사용해서 Agile을 진행하면서 과제를 지속적으로 생성하고 관리함에 있어서 팀원들이 불편함을 많이 느끼고 있어서 툴을 하나 만들어보면서 느낀 점과 소스코드 등에 대해서 다루어보려고 한다.

들어가기에 앞서서, Agile(애자일)이 무엇인지 간략히 말하자면, 레드햇에서는 다음과 같이 정의하고 있다.
"애자일은 신속한 반복 작업을 통해 실제 작동 가능한 소프트웨어를 개발하여 지속적으로 제공하기 위한 소프트웨어 개발 방식입니다. "

 

애자일 방법론이란?

애자일은 신속한 반복 작업을 통해 실제 작동 가능한 소프트웨어를 개발하여 지속적으로 제공하기 위한 소프트웨어 개발 방식입니다.

www.redhat.com

신속한 반복 작업.. 이것을 하기 위해서 JIRA의 티켓을 무수하게 만들어가는 과정 중에 
"자동화를 해서 관리할 수 없나?"
"JIRA 페이지를 다음다음 다음~~ 눌러가면서 생성하는 것 자체도 시간낭비가 많다"
등등의 의견이 팀 내에서 많이 발생하였기에
JIRA API를 사용해서 개발하여
프로젝트 / 라벨 / 담당자 등을 자동으로 기입한 상태로 티켓을 만들어주는 툴을 개발하기로 마음먹었다.

여기서 다루고자 하는 JIRA API는 아래의 페이지를 참고하였습니다.

 

REST APIs

REST APIs The Jira REST APIs are used to interact with the Jira Server applications remotely, for example, when configuring webhooks. The Jira Server platform provides the REST API for common features, like issues and workflows. To get started, read the re

developer.atlassian.com

우선 현재까지 완성된 동작 화면을 보여드리자면, 다음과 같습니다.

로그인화면
로그인인증성공
메인화면

실제로 현장에서 사용 중이고, 이 툴로 인해서 개선된 점은 크게 두 가지가 있다.
1. 티켓을 생성함에 있어서 스트레스가 줄었다.
2. 현재 티켓의 현황을 간략하게 볼 수 있어서 DailyScrum에서 활용하면서 서로의 상황을 이야기하게 되었다.

우선, #1 에서는 로그인 기능에 대해서 다루어보고자 한다.
모든 소스는 깃허브에 등록해두었으니, 더 나은 기능이 있다면, 추가/제어를 해 나가면 좋을 것 같습니다.
느낀 점 피드백 등이 있으면 언제든지 추가할 예정이니 많은 참여 부탁드립니다.

그럼, 시작해보겠습니다.

로그인화면>

Server : 도메인
URL :  도메인/jira/browse/

도메인부분에 사용하시는 JIRA 서버의 URL을 적어주세요.

ID : 실제사용되고있는 유져의 아이디
PW : 실제사용되고있는 유져의 패스워드

사용된 소스는 아래와 같습니다.

using System;
using System.Windows.Forms;

namespace JiraApiControl
{
    public partial class frmLogin : Form
    {
        string id = string.Empty;
        string password = string.Empty;
        string jiraServer = string.Empty;
        string jiraUri = string.Empty;

        public frmLogin()
        {
            InitializeComponent();
        }

        private async void btnLogin_Click(object sender, EventArgs e)
        {
            Jira jira = new Jira();
            if (await jira.Login(txtServer.Text, txtID.Text, txtPW.Text) == false)
            {
                lblResult.Text = "AuthFailed:" + txtID.Text;
                btnRun.Visible = false;
                return;
            }
            else
            {
                jiraServer = txtServer.Text;
                jiraUri = txtURL.Text;
                id = txtID.Text;
                password = txtPW.Text;
                btnRun.Visible = true;

                lblResult.Text = string.Empty;
            }
        }

        /// <summary>
        /// 確認ボタンイベント処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRun_Click(object sender, EventArgs e)
        {
            this.Hide();

            frmMain frmMain = new frmMain(jiraServer, jiraUri, id, password);
            frmMain.Show();
        }

        private void txtPW_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                btnLogin.PerformClick();
            }
        }
    }
}

btnLogin_Click
 -> 인증버튼 클릭이벤트

btnRun_Click
 -> 확인버튼 클릭이벤트

txtPW_KeyDown
 -> 엔터버튼 눌러졌을때 인증버튼을 대신 눌러주는 이벤트

도메인 정보에 대해서는 외부파일로 구성을 하면 좋았을것 같다는 생각도 들었지만,
제가 근무하는 JIRA의 경우에는 5번 패스워드를 틀리면 계정이 Lock 걸리는 문제가 있기때문에
인증정보를 눈으로 봐가면서 진행할 필요가 있어서 부득이하게 로그인 화면을 구성하였습니다.

편의에 따라서 이 화면을 생략하고 사용하셔도 좋을것 같습니다.

위에서 사용된 KeyEventArgs는 아래의 문서에서 상세한 정보를 얻으실수 있습니다.

 

KeyEventArgs 클래스 (System.Windows.Forms)

KeyDown 또는 KeyUp 이벤트에 데이터를 제공합니다.Provides data for the KeyDown or KeyUp event.

docs.microsoft.com

 

C# JIRA REST API 컨트롤에 대한 소스는 아래에 올려두었습니다.

 

sungmanko/JiraRestAPI

This is a JIRA ticket automatic creation and management project created in C#. - sungmanko/JiraRestAPI

github.com

궁금한것이 있으시면 댓글이나, 깃허브에 코멘트 주세요~
#2 메인화면으로 이어집니다.

즐프합시다~

오늘은 입고/출고 기능에 대해서 이어서 글을 써보고자 한다.

전체 공정을 생각해 보았을 때, 1-2주 정도면 필요한 최소의 기능을 장착할 수 있다고 생각한다.

간단한 기능 구현을 하고,

추후에 어떤 식으로 데이터를 관리할 것인지

csv / txt / access / excel 등등.. DataBase를 사용하지 않더라도 얼마든지 가능하므로 조급 해하지 말자.

 

우리가 지난번 위의 그림과 같이 시간을 출력하는 기능까지를 구현해두었다.

이제는 이어서 입고 기능 / 출고 기능을 구현할 차례이다.

 

우선, 첫 번째 생각할 것이

입고 시간 / 출고시간을 버튼 클릭 이벤트와 동시에 어딘가에 보여줘야 한다는 것이다.

코드는 아래와 같이 간단히 구성한다.

        /// <summary>
        /// 입고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnIn1_Click(object sender, EventArgs e)
        {
            lblIn1.Text = DateTime.Now.ToString("HH:mm");
        }

        /// <summary>
        /// 출고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOut1_Click(object sender, EventArgs e)
        {
            lblOut1.Text = DateTime.Now.ToString("HH:mm");
        }

단순히 버튼 옆에 lblIn1 / lblOut1이라는 라벨을 만들어두고

그 값으로 

  입고 버튼 클릭 시에는 lnlIn1의 텍스트 값으로 현재시간의 [시간:분]

  출고 버튼 클릭 시에는 lnlOut1의 텍스트 값으로 현재시간의 [시간:분]

을 보여준다.

 

자.. 실행해서 버튼을 한번 눌러보도록 하자.

 

입고버튼클릭

 

1분 경과 후... 50분이 되었을 때~ 출고 버튼 클릭

 

입고 시간 / 출고시간이 시간:분으로 출력되는 것을 확인할 수 있다.

 

이제는 둘의 차이가 몇 분이 났으며,

30분당 2,000원으로 보았을 때 결국 계산해야 할 금액이 얼마가 필요한 것인지를

알아보도록 하자

 

입고와 출고의 DateTime을 보존하여 보다 편하게 계산하기 위해서 전체 코드를 조금 수정하기로 한다.

전체 코드는 다음과 같다.

using System;
using System.Windows.Forms;

namespace ParkingManagement
{
    public partial class frmParkingManagement : Form
    {
        /// <summary>
        /// 주차장1의 인/아웃 시간관리
        /// </summary>
        private DateTime parking1_In;
        private DateTime parking1_Out;

        /// <summary>
        /// 컨스트럭터
        /// </summary>
        public frmParkingManagement()
        {
            InitializeComponent();
        }
        
        /// <summary>
        /// 입고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnIn1_Click(object sender, EventArgs e)
        {
            parking1_In = DateTime.Now;
            lblIn1.Text = parking1_In.ToString("HH:mm");
        }

        /// <summary>
        /// 출고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOut1_Click(object sender, EventArgs e)
        {
            parking1_Out = DateTime.Now;
            lblOut1.Text = parking1_Out.ToString("HH:mm");

            // (주차시간 / 단위시간) * 단위금액
            TimeSpan ts = parking1_Out - parking1_In;
        }
        
        /// <summary>
        /// 화면로드이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void frmParkingManagement_Load(object sender, EventArgs e)
        {
            timerMain.Start();
        }

        /// <summary>
        /// Timer Tick 이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerMain_Tick(object sender, EventArgs e)
        {
            lblNowTime.Text = DateTime.Now.ToString("HH:mm:ss");
        }
    }
}

 

바뀌는 부분을 설명하자면,

        /// <summary>
        /// 주차장1의 인/아웃 시간관리
        /// </summary>
        private DateTime parking1_In;
        private DateTime parking1_Out;

주차장 1의 인/아웃 버튼 클릭 시에 사용될 DateTime 즉, 버튼 클릭 이벤트 당시의 시간을 기록하여 담고 있을 공간을 지역변수로 외부 선언하였고

인/아웃의 버튼 클릭이벤트를 아래와 같이 바꾸어주었다.

        /// <summary>
        /// 입고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnIn1_Click(object sender, EventArgs e)
        {
            parking1_In = DateTime.Now;
            lblIn1.Text = parking1_In.ToString("HH:mm");
        }

        /// <summary>
        /// 출고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOut1_Click(object sender, EventArgs e)
        {
            parking1_Out = DateTime.Now;
            lblOut1.Text = parking1_Out.ToString("HH:mm");

            // (주차시간 / 단위시간) * 단위금액
            TimeSpan ts = parking1_Out - parking1_In;
        }
        

 

이유는, 아웃(출고) 이벤트를 수행할 때에 시간을 기록하고 있으므로,

따로 또 다른 형 변환을 시키지 않고, 처음 이벤트가 실행될 때에 값을 그대로 저장해두면 추가로 허튼짓?을 안 해도 되기 때문이다. 

※물론, 이렇게 하는 것이 100% 맞다는 것은 아니므로 형 변환을 통해서 값을 계산하여도 좋다.

 

        /// <summary>
        /// 출고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOut1_Click(object sender, EventArgs e)
        {
            parking1_Out = DateTime.Now.AddMinutes(41);
            lblOut1.Text = parking1_Out.ToString("HH:mm");

            // 주차시간계산
            TimeSpan ts = parking1_Out - parking1_In;
            CalcSum(ts);
        }

        /// <summary>
        /// 주차요금계산
        /// </summary>
        /// <param name="ts">시간차</param>
        /// <returns>요금계산결과</returns>
        private string CalcSum(TimeSpan ts)
        {
            return (Math.Ceiling(Convert.ToDecimal(ts.Minutes / Convert.ToDecimal(txtBaseTime.Text))) 
                * Convert.ToInt32(txtBaseMoney.Text)).ToString();
        }

내 나름대로 계산 모듈을 구현하면 위와 같이 된다.

출고 버튼 클릭이벤트에서는 지역변수에서 가지고 있던 값을 통해서 시간 차이를 구현하였고,

 

추차요금계산 메서드에서는 좀 난잡하지만, 시간차에 따른 요금계산을 실시하고 있다.

우선, 요금계산에 대해서 천천히 하나하나 살펴보도록 하자.

 

        /// <summary>
        /// 주차요금계산
        /// </summary>
        /// <param name="ts">시간차</param>
        /// <returns>요금계산결과</returns>
        private string CalcSum(TimeSpan ts)
        {
            return (Math.Ceiling(Convert.ToDecimal(ts.Minutes / Convert.ToDecimal(txtBaseTime.Text))) 
                * Convert.ToInt32(txtBaseMoney.Text)).ToString();
        }

 

Math.Ceiling이라는 녀석은 반올림/올림의 올림이다.

더 자시한 내용은 아래의 링크를 참고하자.

 

Math.Ceiling 메서드 (System)

지정된 수보다 크거나 같은 최소 정수 값을 반환합니다.Returns the smallest integral value greater than or equal to the specified number.

docs.microsoft.com

txtBaseTime.Text 베이스타임을 Decimal형태로 바꿔주고 시간차로(분으로 환산) 나누어준다.

    시간차 / 베이스타임

의 수식인셈이다.

 

이것은 소수점을 가지는 값이 되므로(물론, 나누어서 딱 떨어져서. 0 이 될 때도 있다)

Convert.ToDecimal로 전체 값을 형 변환해줄 필요가 있다.

 

그리고 그 값을 Ceiling으로 올림 처리해준다.

왜, 여기서 올림이냐??

 

단위 시간이 30분이고 2,000원을 요금으로 받겠다고 하였을 때로 가정하면 

   1분 ~ 30분까지의 주차요금은 2,000원

  31분 ~ 60분까지의 주차요금은 4,000원 

이 되어야 하기 때문이다.

 

여기까지 정리한 최종 소스는 아래와 같다.

using System;
using System.Windows.Forms;

namespace ParkingManagement
{
    public partial class frmParkingManagement : Form
    {
        /// <summary>
        /// 주차장1의 인/아웃 시간관리
        /// </summary>
        private DateTime parking1_In;
        private DateTime parking1_Out;

        /// <summary>
        /// 컨스트럭터
        /// </summary>
        public frmParkingManagement()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 입고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnIn1_Click(object sender, EventArgs e)
        {
            parking1_In = DateTime.Now;
            lblIn1.Text = parking1_In.ToString("HH:mm");
        }

        /// <summary>
        /// 출고이벤트_1
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnOut1_Click(object sender, EventArgs e)
        {
            parking1_Out = DateTime.Now.AddMinutes(41);
            lblOut1.Text = parking1_Out.ToString("HH:mm");

            // 주차시간계산
            lblSum1.Text = CalcSum(parking1_Out - parking1_In);
        }

        /// <summary>
        /// 주차요금계산
        /// </summary>
        /// <param name="ts">시간차</param>
        /// <returns>요금계산결과</returns>
        private string CalcSum(TimeSpan ts)
        {
            return (Math.Ceiling(Convert.ToDecimal(ts.Minutes / Convert.ToDecimal(txtBaseTime.Text)))
                * Convert.ToInt32(txtBaseMoney.Text)).ToString();
        }

        /// <summary>
        /// 화면로드이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void frmParkingManagement_Load(object sender, EventArgs e)
        {
            timerMain.Start();
        }

        /// <summary>
        /// Timer Tick 이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerMain_Tick(object sender, EventArgs e)
        {
            lblNowTime.Text = DateTime.Now.ToString("HH:mm:ss");
        }
    }
}

 

 

또한 현재 진행 내용에 대해서는 

github에 소스를 올려두고, 하나씩 하나씩 기능도 같이 추가해 나아가고자 한다.

 

github.com/sungmanko/ParkingManagement

 

sungmanko/ParkingManagement

ParkingManagement. Contribute to sungmanko/ParkingManagement development by creating an account on GitHub.

github.com

소스는 위의 github에서 다운로드할 수 있다.

교육을 진행하면서 필요한것들과 느낀점들을 정리해본다.

개별적인 학습을 목적

 

<주제>

주차장관리시스템

 

<생각할것>

흔히 우리가 목격하게되는 주차장 시스템은 어떤식으로 이루어져있는가?

 

1. 입고

2. 정산

3. 금액을 지불

4. 출고

 

무인시스템의 주차장 관리 시스템을 만들어보도록한다.

 

폼의 디자인

주차관리시스템

 

입고버튼 : 차량이 입고되었을때의 이벤트

 - 라벨 = 입고시간

출고버튼 : 차량이 출고되었을때의 이벤트

 - 라벨 = 현재시간과 입고시간을 계산해서 몇분 주차중인지를 보여줌

현재금액 : 단위시간과 더불어서 현재 정산해야할 금액을 리얼타임표시 (1분단위 갱신)

 

타이머배치 : 화면상은 보이지않지만, 현재시간과 입고시간의 차이를 이용해서 금액을 보여줘야하므로 디자이너에 하나 추가해준다. (코드상에 넣어도 무관)

 

타이머 인터벌1000(1초)

        /// <summary>
        /// 화면로드이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void frmParkingManagement_Load(object sender, EventArgs e)
        {
            timerMain.Start();
        }

frmParkingManagement_Load 이벤트에서 타이머를 스타트해준다.

 

그리고 여기까지 준비가 되었으므로

타이머의 Tick 이벤트에 아래와같이 코드를 추가해준다.

        /// <summary>
        /// Timer Tick 이벤트
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timerMain_Tick(object sender, EventArgs e)
        {
            lblNowTime.Text = DateTime.Now.ToString("HH:mm:ss");
        }

<참고> 

 

Timer 클래스 (System.Windows.Forms)

사용자가 정의한 간격마다 이벤트를 발생시키는 타이머를 구현합니다.Implements a timer that raises an event at user-defined intervals. 이 타이머는 Windows Forms 애플리케이션에서 사용할 수 있도록 최적화되었

docs.microsoft.com

DateTime.Now 는 현재의 시스템시간을 나타내는것이고 

ToString("HH:mm:ss"); 이부분은 시스템시간 전체에서 HH는 시간 / mm 은 분 / ss 는 초 를 출력하겠다는 설정이다.

 

 

DateTime 구조체 (System)

일반적으로 날짜와 시간으로 표시된 시간을 나타냅니다.Represents an instant in time, typically expressed as a date and time of day.

docs.microsoft.com

 

 

여기까지 했으면 한번 실행을 해보자.

아래와같이 될것이다.

 

 

지금까지 만든내용의 실행결과

일단 

여기까지 완성되었다.

 

다음글에서는 입고기능 / 출고기능 에 대해서 다루어 보도록 하겠다.

 

 

소스는 아래에서 최신판을 다운받을수있습니다.

 

sungmanko/ParkingManagement

ParkingManagement. Contribute to sungmanko/ParkingManagement development by creating an account on GitHub.

github.com

 

시간이 좀 지체되었지만, 요걸 하고 있는 과정 중에도 여러 가지 문제가 생겼다.

뜻밖의 얘기들을 납품 직전에 거래처에서 듣게 되었는데...

사용할 브라우저가 IE 가 아니라 Chrome이라고 한다..

어처구니없지만

또 해야하는게 우리 엔지니어의 숙명인지라 하는 수없이 또 고민을 하였기에 남겨본다.

 

레지스트리 너란녀석....

 

RegistryValueKind 열거형 (Microsoft.Win32)

레지스트리에 값을 저장할 때 사용할 데이터 형식을 지정하거나 레지스트리 값의 데이터 형식을 나타냅니다.Specifies the data types to use when storing values in the registry, or identifies the data type of a value in

docs.microsoft.com


우선 크롬으로 테스트할 html 페이지를 하나 만들어본다.

<!DOCTYPE html>
<html>
<head>
 <title>SampleSystem</title>
 <meta http-equiv="content-type" charset="shift-jis">
</head>
<style type="text/css"></style>
<script>

function execute(){
	var target1 = document.getElementsByName("param1")[0].value;
	var target2 = document.getElementsByName("param2")[0].value;
	var target3 = document.getElementsByName("param3")[0].value;
	var target4 = document.getElementsByName("param4")[0].value;
	var target5 = document.getElementsByName("param5")[0].value;

	var check1 = "0";	
	var check2 = "";	
	var check3 = "0";	
	var check4 = "";	
	var check5 = "";	
	for (var i = 0; i < document.Disconnection.DisconnectionFlag.length; i++) {
		if(i==0){
			if (document.Disconnection.DisconnectionFlag[i].checked) {
				check2 = "1";
			} else {
				check2 = "0";
			}
		}
		if(i==1){
			if (document.Disconnection.DisconnectionFlag[i].checked) {
				check5 = "1";
			} else {
				check5 = "0";
			}
		}
		if(i==2){
			if (document.Disconnection.DisconnectionFlag[i].checked) {
				check4 = "1";
			} else {
				check4 = "0";
			}
		}
	}

	var target6 = check1 + check2 + check3 + check4 + check5;

	location.href = "SampleSystem:" + "/" + target1 + "/" + target2 + "/" + target3 + "/" + target4 + "/" + target5 + "/" + target6;
}

</script>
<br>
SAMPLE
<form name="Disconnection" style="padding-left:20px">
  test input1 : <input type="text" name="param1" value="test"><p>
  test input2 : <input type="text" name="param2" value="sample"><p>
  test input3 : <input type="text" name="param3" value="092-1111-1111"><input type="checkbox" class="bigCheck" name="DisconnectionFlag" style="position:relative; top:5px;"><p>
  test input4 : <input type="text" name="param4" value="080-2222-2222"><input type="checkbox" class="bigCheck" name="DisconnectionFlag" style="position:relative; top:5px;"><p>
  test input5 : <input type="text" name="param5" value="050-3333-3333"><input type="checkbox" class="bigCheck" name="DisconnectionFlag" style="position:relative; top:5px;"><br>
</form>

<input type="button" name="summit" style="position:relative; left:20px; top:0px; height:50px; width:200px;" value="SampleSystemAction" onclick="execute()">

※필자는 이미 SampleSystem이라는 호출되는 대상의 프로그램이 존재하고 있습니다.

  호출될 대상의 프로그램은 초기에 실행될때에 args를 요구하고 있으며, "/"를 split 해서 사용할 예정입니다.

 

페이지를 작성하였으면 일단 그대로 한번 불러봅니다.

그러면 경고창이 나오면서 바로 실행은 되지 않는것을 볼 수 있을 것입니다.

이때에 필요한 것이 아래의 레지스트 제어입니다.

 

크롬을 제어하는경우

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome]
"ExternalProtocolDialogShowAlwaysOpenCheckbox"=dword:00000001

 

인터넷 익스플로러를 제어하는 경우

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\ProtocolExecute\samplesystem]
"WarnOnOpen"=dword:00000000

 

그리고 마지막으로 내 프로그램을 실행할때에 필요한 레지스트리 제어를 해주도록 한다.

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\SampleSystem]
@="URL:SampleSystem Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\SampleSystem\DefaultIcon]

[HKEY_CLASSES_ROOT\SampleSystem\shell]

[HKEY_CLASSES_ROOT\SampleSystem\shell\Open]

[HKEY_CLASSES_ROOT\SampleSystem\shell\Open\command]
@="\"C:\\Contact\\SampleSystem\\SampleSystem.exe\" \"%1\""

 

여기까지 하였다면 이제

처음에 만들었던 html 파일을 크롬 혹은 IE에서 실행해보자

 

대상프로그램이 실행 파라메터를 5개 요구하는 프로그램이 준비되어 있어서 저는5개를 보내도록 했습니다.

 

그리고 페이지 표시가 되었다면 버튼을 한번 눌러봅시다.

 

오오... 아무런 경고창없이 그대로 내 컴퓨터에 설치되어 있는, 프로그램이 실행되었습니다~

로컬PC에 설치해둔 프로그램을 웹에서 실행한 모습

 

레지스트리 제어파일의 배포는 인스톨러 작성시에 추가하실수 있으니 참고하세요.

 

 

 

c#, UWF, WPF

★ shift キーを押しながら、リンクをクリックすると、新規タブでページが見れます。★

【WPF】

Windows 10 開発の概要

 

Windows 10 開発の概要 - Learn

Windows 10 開発について理解し、重要なツールをインストールして、Windows Insider Program について確認します。

docs.microsoft.com

初の Windows 10 アプリケーションを作成する

 

最初の Windows 10 アプリケーションを作成する - Learn

最も人気のある Windows 開発フレームワークである UWP、WPF、Windows フォームでアプリを作成する方法を学習します。

docs.microsoft.com

Windows 10 アプリのユーザー インターフェイス (UI) を作成する

 

Windows 10 アプリのユーザー インターフェイス (UI) を作成する - Learn

Windows 10 アプリ用の UI を作成するための基礎について説明します。要素を組み合わせて、UWP、WPF プラットフォームのいずれかで、目的の UI を作成します。

docs.microsoft.com

Windows 10 アプリのユーザー インターフェイスを拡張する

 

Windows 10 アプリのユーザー インターフェイス (UI) を拡張する - Learn

Windows 10 アプリの UI にナビゲーションとメディアを追加し、ユーザー エクスペリエンスを向上させる方法について説明します。

docs.microsoft.com

------------------------------------------------

★ shift キーを押しながら、リンクをクリックすると、新規タブでページが見れます。★

【C#】

C# の最初のステップ

 

C# の最初のステップ - Learn

プログラミング言語の学習に関心があるのに、どこから始めればよいかわからない場合は、 ここから始めましょう。 C# で単純なアプリケーションを構築するために必要な基本的な構文と思

docs.microsoft.com

最初の C# コードを記述する

 

最初の C# コードを記述する - Learn

C# 構文の基本を理解するために、小さなコード例を記述することから始めましょう。

docs.microsoft.com

C# でリテラル値と変数値を使用してデータを格納および取得する

 

C# でリテラル値と変数値を使用してデータを格納および取得する - Learn

さまざまなデータ型のリテラル値と変数値を作成して、ご利用のアプリケーションでデータを使用します。

docs.microsoft.com

C# で基本的な文字列を書式設定する

 

C# で基本的な文字列を書式設定する - Learn

特殊文字、書式設定、Unicode を使用して入力されたリテラルと変数のテキスト データを結合し、エンド ユーザーにとって意味のあるメッセージを作ります。

docs.microsoft.com

C# で数値に基本的操作を実行する

 

C# で数値に基本的操作を実行する - Learn

数値データに基本的算術演算を実行するための演算子と手法について説明します。

docs.microsoft.com

C# を使用して .NET クラス ライブラリからメソッドを呼び出す

 

C# を使用して .NET クラス ライブラリからメソッドを呼び出す - Learn

値を返したり、入力パラメーターを受け入れたりするメソッドを呼び出して、.NET クラス ライブラリの機能を使用します。

docs.microsoft.com

C# で if-elseif-else ステートメントを使用して決定ロジックをコードに追加する

 

C# で if-elseif-else ステートメントを使用して決定ロジックをコードに追加する - Learn

ブール式を評価してコードの実行パスを分岐する方法を学習します。

docs.microsoft.com

C# で配列と foreach ステートメントを使用して、データのシーケンスを格納し、反復処理する

 

C# で配列と foreach ステートメントを使用して、データのシーケンスを格納し、反復処理する - Le

配列と呼ばれるデータ構造で、関連するデータのシーケンスを操作します。 その後、シーケンス内の各項目を反復処理する方法を学習します。

docs.microsoft.com

C# で規則、空白文字、コメントを使用して読みやすいコードを作成する

 

C# で規則、空白文字、コメントを使用して読みやすいコードを作成する - Learn

名前付け規則、コメント、および空白文字を使用して、読みやすく、更新やサポートの容易なコードを記述します。

docs.microsoft.com

 

어찌 보면 단순한 이야기일 수도 있겠지만, 실제로 하려다 보니 여러 가지 문제가 생겨서 
해결 과정을 글로 남겨두려고 한다.

하려고 하는것은 WebPage에서 로컬 PC 내부에 저장되어있는 exe 파일을 실행하는 것이다.

처음 시작하기전에 생각한 것은 아래와 같다.

1. ActiveX를 사용해서 로컬PC의 파일을 다이렉트로 실행한다. 
2. 레지스트 등록을 통해서 정해진 키값을 WebPage내부에서 Script 호출해서 사용한다. 
3. 브라우져에서 파일접근 확인창이 매번 뜨지않도록 제어한다.(Chrome, IE, Edge)

 

■참고

ActiveX 사용에 대한 참고문서

 

방법: ActiveX 컨트롤 사용 - Visual Basic

방법: ActiveX 컨트롤을 사용하여 작업(Visual Basic)How to: Work with ActiveX Controls (Visual Basic) 이 문서의 내용 --> ActiveX 컨트롤은 웹 페이지 또는 다른 응용 프로그램에 삽입 하 여 다른 사용자가 프로그래

docs.microsoft.com

WindowsAPI호출

 

연습: Windows API 호출 - Visual Basic

연습: Windows API 호출(Visual Basic)Walkthrough: Calling Windows APIs (Visual Basic) 이 문서의 내용 --> Windows Api는 Windows 운영 체제의 일부인 Dll (동적 연결 라이브러리)입니다.Windows APIs are dynamic-link libraries (DLLs)

docs.microsoft.com

시작 전에 생각한 문제점
1. 인터넷 브라우저에 특정되지 않는가? (IE 외에는 ActiveX 실행이 어렵지 않을까?)
2. 레지스트 등록은 관리자 권한이 없으면 안 된다.


천리길도 한걸음부터….
우선, 하나하나 시작해보도록 한다.

1. ActiveX를 사용해서 로컬 PC의 파일을 디렉트로 실행한다.

<script LANGUAGE="JavaScript">
function cmd() {
    var obj = new ActiveXObject("WScript.Shell");
    obj.Run("C:\\test\\sample.exe");
}
</script>
<body>
---ActiveX Sample---<br><br>
<a href="javascript:cmd()">ActiveX sample</a><br>
</body>

단순히 위와 같은 코드로 효과를 볼 수가 있다.
실행하면 아래와 같이 된다.

아랫부분의 하이퍼링크를 클릭하면 sample.exe 가 실행된다.
하지만, 예외 없이 경고창이 등장한다. (필자가 일본에 있어서, 일본어판 윈도를 사용하고 있습니다.)

이 페이지의 ACtiveX 컨트롤은 안전하지 않을 가능성이 있고, 페이지 외의 부분에서 영향을 끼칠 수 있습니다. 다른 부분의 영향에 대해 문제가 없습니까?

라고 나오는데, 뭐 실행을 시켜야 하니까 그냥 Y로 속행~~ 하면, 실행이 된다.

다음 편에서는 

레지스트 등록을 통해서 정해진 키값을 WebPage내부에서 Script 호출해서 사용한다. 

에 대해서 다루어 보도록 하겠다.

WPF프로젝트를 지난 5년간 진행하면서 덩치가 너무 커진 탓인지
속도가 너무 늦다는 지적을 현장에서 받아오면서, 이 문제를 어떻게 처리할까... 한참을 고민하던 끝에 다음 달 릴리즈를 앞둔 마당에 이 글을 적어봅니다.

초기 표시 (렌더링) 속도 개선방법
여기에서는 초기 표시, 즉 화면을 인스턴스 화해서 그려질 때까지의 시간을 단축하기 위한 개선책과
화면을 다시 그릴 때의 처리시간 단축 방법을 이야기하고자 합니다.

 

렌더링, WPF레이아웃이 무엇이냐?? 아래의 마이크로소프트 링크를 참고하세요.

 

완화: WPF 창 렌더링

Windows 8 이상에서 실행되는 .NET Framework 4.6에서 WPF 창 렌더링의 영향 및 완화에 대해 알아봅니다.

docs.microsoft.com

 

 

완화: WPF 레이아웃

한 픽셀씩 이동하는 개체의 배치와 같이 WPF 컨트롤 레이아웃을 변경하여 발생하는 문제를 완화하는 방법에 대해 알아봅니다.

docs.microsoft.com

 

 

1. 컨트롤의 재배치 처리 횟수를 줄이자
  WPF는 기본적으로 화면의 각 요소는 상대적 좌표 크기를 결정합니다.
  컨트롤의 "HorizontalAlignment"또는 "Width"에 "Auto"를 설정할 수 있는 것이 그 증거입니다.
  이렇게 함으로써 사용자가 화면 크기를 변경하거나 해상도가 다른 디스플레이에서도
  화면의 표시내용을 담아내는 것을 제작하는 것이 가능하게 되어 있습니다.

  그러나 이때의 배치 구조는 Xaml 부모 요소에서 자식 요소에 여러 번 왕래하고 크기를 조정하고
  최종 렌더링 된다는 것입니다.
  이 재배치 처리에 시간이 걸리면 당연히 표시할 때까지의 시간도 늦어집니다.

  이 문제를 해결하려면 다음 방법을 검토하십시오.

크기를 고정한다.
  ⇒Width / Height는 고정값으로 합니다.
부모 패널을 Canvas 한다.
  ⇒ 절대 좌표로 지정하여 그리기 좌표를 재계산하지 않아도 됩니다.
문자 혹은 도형을 코드로 렌더링 한다.
  ⇒ Xaml 대신 코드 측에서 직접 그리는 방법입니다.
   자세한 내용은 코드로 문자 나 도형을 그리기를 참조하십시오.

2. 자신 컨트롤을 UserControl에서 사용자 지정 컨트롤로 변경한다.
  WPF에서 컨트롤을 만드는 방법은 주로 다음 두 가지입니다.
  (1) UserControl에서 컨트롤을 복합적으로 사용한다.
  (2) 사용자 지정 컨트롤 모양과 구조를 직접 구현한다.
    (또는 기존의 컨트롤을 상속 기능을 추가한다.)

  기본적인 것이지만, 컨트롤 클래스는 파생할 정도로 기능이 증가 · 복잡화하고
  재배치 처리에 시간이 걸릴 수 있습니다.
  UserControl은 Window와 마찬가지로 View를 가지고 있기 때문에 재배치 처리에 시간이 걸립니다. 
  UserControl의 수가 증가할수록 내부의 요소가 복잡할수록 그것은 눈에 띄게 됩니다.

  이를 방지하려면 사용자 정의 컨트롤을 구현하십시오.
  사용자 정의 컨트롤을 구현하는 프로젝트는 Visual Studio에 포함되어 있습니다.
  (프로젝트의 추가 메뉴에서 " WPF 사용자 정의 컨트롤 라이브러리"라는 항목이 있습니다.)
  일반 프로젝트에 사용자 지정 컨트롤을 추가하지 않도록 주의합시다.

3. 의존관계 속성을 정확히 사용한다.
  의존관계 속성은 데이터 바인딩을 사용하는 구조에서
  사용자 컨트롤의 속성은 기본적으로 이를 구현합니다.
  일반적 속성도 처음에만 데이터 바인딩은 작동하지만,
  이후의 속성 값의 변화에 대응할 수 없습니다.
  또한 일반적인 속성은 런타임에 데이터 바인딩을 사용할 때,
  리플렉션을 이용하고 있기 때문에 의존관계 속성보다 속도가 느립니다.

4. 렌더링 속도를 향상할 수 있는 속성
  사용자 컨트롤의 기본 클래스 "Control"또는 "FrameworkElement"에는
  값을 설정하여 렌더링 속도를 향상시킬 수있는 몇 가지 속성이 존재합니다.
  Xaml의 상대적인 레이아웃 설정의 장점을 없애는 것도 있지만,
  다음의 속성을 설정하는 것을 고려하십시오.

· IsHitTestVisible (기본값 : false)
   true로 설정하면 컨트롤을 수행할 수 없습니다.
   마우스 오버와 클릭 등의 작업을 일체 행하지 않습니다.
   조작이 불필요한 컨트롤에 이것을 설정하여 불필요한 이벤트가 발생하지 않고,
   속도의 향상을 기대할 수 있습니다.

· ClipToBounds (기본값 : false)
   true로 설정하면 컨트롤의 자식 요소가 자신의 그리기 범위를 벗어난 영역에 그려지지 않습니다.
   그리기 영역이 한정되기 때문에, 동작 속도의 향상을 기대할 수 있습니다.
   그러나 재배치 작업이 발생하는 점에 주의하십시오.

· Width / Height (기본값 : Auto)
   고정 값으로 설정하면 재배치 처리 횟수를 줄일 수 있습니다.
   마찬가지로 "MaxWidth", "MinWidth", "MaxHeight", "MinHeight"도 고정 값으로 설정하는 것을 권장합니다.

UserControl 전반적으로 위의 문제를 다시 한번 생각하고 재배치하여, 
기존 3초 정도의 렌더링 완료가 되었던 화면이 1초 만에 완료되도록 개선되었기에 
정보를 정리해두도록 합니다.

파일을 Drag&Drop해서 프리뷰 하는 방법에 대해서 간략하게 적어본다.

일단, 아래의 링크에 접속해서  파일을 다운로드 받아야한다.

http://www.adobe.com/devnet/acrobat/sdk/eula.html

자신의 환경에 맞는 파일을 다운받도록 합니다.

<소스관련>
화면구성은 아래와 같습니다. (일본에서 개발중이라, 일본어가 섞여 있습니다.)

메인화면 디자인

화면을 구성한 컨트롤은 아래와 같다.

리스트박스 1
PrintPreview 1
PictureBox 1
printDocument 1


실행하면, 아래와 같은 동작을 합니다.

<실행화면>

리스트박스에 엑셀파일, PDF 파일을 드래그 앤 드롭 실시

파일 드래그 앤 드롭
드래그 앤 드롭 후
PDF파일 더블클릭

물론, PDF 파일이기때문에 양쪽에 표시된 화살표를 클릭해서 추가 기능을 열고 접을수 있습니다.



코드는 아래와 같습니다.

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

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

        //ListBox1のDragEnterイベントハンドラ
        private void ListBox1_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)
        {
            //コントロール内にドラッグされたとき実行される
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
                //ドラッグされたデータ形式を調べ、ファイルのときはコピーとする
                e.Effect = DragDropEffects.Copy;
            else
                //ファイル以外は受け付けない
                e.Effect = DragDropEffects.None;
        }

        //ListBox1のDragDropイベントハンドラ
        private void ListBox1_DragDrop(object sender, System.Windows.Forms.DragEventArgs e)
        {
            //コントロール内にドロップされたとき実行される
            //ドロップされたすべてのファイル名を取得する
            string[] fileName = (string[])e.Data.GetData(DataFormats.FileDrop, false);
            //ListBoxに追加する
            listBox1.Items.AddRange(fileName);
        }

        private void pictureBox1_Click(object sender, EventArgs e)
        {
            if (listBox1.SelectedItem != null)
            {
                DialogResult dr = MessageBox.Show("指定したファイルを削除しますか?", "【ファイル削除】", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);

                if (dr == DialogResult.OK)
                {
                    System.IO.File.Delete(listBox1.SelectedItem.ToString());
                    listBox1.Items.Remove(listBox1.SelectedItem);
                }
            }
            else
            {
                MessageBox.Show("選択されたアイテムがありません。", "【ファイル削除】", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void listBox1_DoubleClick(object sender, EventArgs e)
        {
            // 選択したファイルが存在する場合
            if (listBox1.SelectedItem != null)
            {
                // 拡張子を取得
                string extensionKey = System.IO.Path.GetExtension(listBox1.SelectedItem.ToString()).ToLower();

                switch (extensionKey)
                {
                    case ".pdf":     // PDFファイル
                        // ブラウザコントロールの作成
                        axAcroPDF1.Visible = true;
                        printPreviewControl1.Visible = false;

                        axAcroPDF1.LoadFile(listBox1.SelectedItem.ToString());
                        break;

                    case ".csv":     // CSVファイル
                        axAcroPDF1.Visible = false;
                        printPreviewControl1.Visible = true;

                        printPreviewControl1.Document = printDocument1;

                        break;

                    case ".xlsx":    // EXCELファイル
                        axAcroPDF1.Visible = false;
                        printPreviewControl1.Visible = true;

                        break;

                    default:
                        axAcroPDF1.Visible = false;
                        printPreviewControl1.Visible = false;

                        /* 特定できてない拡張子が来た場合を想定 */
                        break;
                }
            }
        }

        private void printDocument1_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
        {
            //画像を読み込む
            Image img = Image.FromFile(listBox1.SelectedItem.ToString());
            //画像を描画する
            e.Graphics.DrawImage(img, 0, 0, img.Width, img.Height);
            //次のページがないことを通知する
            e.HasMorePages = false;
            //後始末をする
            img.Dispose();
        }
    }
}


소스에서 xlsx(엑셀) csv에 대해서는 아직 구현하지 않았고 보류중인 기능입니다.
참고로, 아래의 링크에 현재 구성된 소스 

사용된 소스 전체는 아래의 링크에서 다운받을수 있습니다.
아래의 계정을 github에서 follow 해두시면, 수정 및 업데이트 내용을 알람으로 받으실수 있습니다.

https://github.com/sungmanko/DragAndDrop

 

sungmanko/DragAndDrop

DragAndDrop Sample. Contribute to sungmanko/DragAndDrop development by creating an account on GitHub.

github.com

 

추가로 궁금하신점이 있으시면 아래의 링크를 통해서도 보다 많은 정보를 얻으실수 있습니다.
도움되시길 바랍니다.

<C#가이드>

 

C# 문서 - 시작, 자습서, 참조.

프로그래밍 C# 알아보기 - 초보 개발자, C#을 처음 사용하는 개발자, 숙련된 C# / .NET 개발자용

docs.microsoft.com

<C# 8.0의 새로운 기능>

 

C# 8.0의 새로운 기능 - C# 가이드

C# 8.0의 새로운 기능을 살펴봅니다.

docs.microsoft.com

<VB가이드>

 

Visual Basic 설명서 - 시작, 자습서, 참조.

.NET의 Visual Basic 프로그래밍 알아보기 - 초보 개발자, Visual Basic을 처음 사용하는 개발자, 숙련된 Visual Basic 개발자

docs.microsoft.com

<VB새로운기능>

 

C# 문서 - 시작, 자습서, 참조.

프로그래밍 C# 알아보기 - 초보 개발자, C#을 처음 사용하는 개발자, 숙련된 C# / .NET 개발자용

docs.microsoft.com

 

ShowDialog로 호출되던 폼을 Show로 바꾸면서 화면비표시를 하고싶다.


프로젝트는 C#으로 이루어져 있으며, VB6.0과 연동되어 실행되고 있었다.

구성은 아래와 같다.


문제는 해당폼이 Modal 기동된다는 점이다.
Modal 띄워놓고, 화면은 보여주면 안된다.
이것을 어떻게 해결할것인가…
나는 이번 수정을 두가지 방법으로 생각해보았다.
1. 해당폼을 생성하지않고, Load / Shown 이벤트의 내용을 그대로 메소드1에서 수행한다.
2. 해당폼을 보여주지않도록 수정하고, 그냥 진행한다.

일단, 비동기 형식으로 접근을 생각해보았다.
솔류션을 새로생성하고,폼을 두개가 되도록 배치한다.

그리고, 폼1은 다음과같이 디자인하였다.

결국 버튼2만 사용하여 해결하였지만,
여기서 중요한것은 폼의 객체는 생성하지만, 유져에게 보여주어서는 안된다는점.
그리고 Load / Shown 이벤트는 그대로 진행되어야 한다는점.
두가지의 미션이 가장 중요한 부분이다.
해당 이벤트가 불리어지는지는 어떻게 확인할까?
아래와 같이 확인해보았다.


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
using System;
using System.Windows.Forms;
 
namespace ApcEventControl
{
    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }
 
        private void Form2_Load(object sender, EventArgs e)
        {
            MessageBox.Show("Form2_Load");
        }
 
        private void Form2_FormClosing(object sender, FormClosingEventArgs e)
        {
            MessageBox.Show("Form2_FormClosing");
        }
 
        private void Form2_Shown(object sender, EventArgs e)
        {
            MessageBox.Show("Form2_Shown");
        }
 
        private void Form2_FormClosed(object sender, FormClosedEventArgs e)
        {
            MessageBox.Show("Form2_FormClosed");
        }
    }
}
 
cs

폼2에 각각의 이벤트를 생성해서, 메세지박스로 표시
가장 단순하면서, 가장 알아보기 쉬운방법이다.
뭐, 로그로 표현할수도 있었지만, 이번에는 그냥 메세지박스로 표시하기로 한다.

폼1은 아래와같이 구현하였다.

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
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace ApcEventControl
{
    public partial class Form1 : Form
    {
        Form2 fm2;
 
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            fm2 = new Form2();
            fm2.WindowState = FormWindowState.Minimized;
            fm2.ShowDialog();
 
            fm2.ShowInTaskbar = false;
 
            fm2.Hide();
        }
 
        private int XXX()
        {
            fm2 = new Form2();
            fm2.FormClosed += Fm2_FormClosed;
            fm2.WindowState = FormWindowState.Minimized;
            fm2.ShowInTaskbar = false;
            fm2.Show();
 
            var task = Task.Run(() =>
            {
                while (fm2 != null)
                {
                    System.Threading.Thread.Sleep(100);
                }
 
                string a = "";
 
                MessageBox.Show("1");
                MessageBox.Show("2");
                MessageBox.Show("3");
                
                if (a == string.Empty)
                {
                    return 0;
                }
                else
                {
                    return 1;
                }
            });
 
            return 9;
        }
 
        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show(XXX().ToString());
        }
        
        private void Fm2_FormClosed(object sender, FormClosedEventArgs e)
        {
            fm2 = null;
        }
 
        private void button3_Click(object sender, EventArgs e)
        {
            fm2 = new Form2();
            fm2.WindowState = FormWindowState.Minimized;
            fm2.Hide();
            fm2.ShowInTaskbar = false;
            fm2.Show();
 
            MessageBox.Show("1");
            MessageBox.Show("2");
            MessageBox.Show("3");
        }
        
        private void button4_Click(object sender, EventArgs e)
        {
            fm2.Close();
        }
    }
}
 
cs

버튼2에서의 처리프로세스
즉, XXX 라고 지칭해둔 함수를 통한 작업이 수행된다.
여기서 중요한것은 Form2 의 인스턴스를 Form1에 private 선언해두고, 해당 인스턴스를 감시한다는점이다.

1
2
            fm2 = new Form2();
            fm2.FormClosed += Fm2_FormClosed;
cs

formClosed 이벤트를 확장해두었다.


1
2
3
4
        private void Fm2_FormClosed(object sender, FormClosedEventArgs e)
        {
            fm2 = null;
        }
cs

내부처리는 위와같다.
form2의 private 객체를 null로 변환한다.


XXX 메소드에서 Task 를 발행해서, 해당객체가 null이 될때까지 감시하도록 하였으니,
이 객체값으로 null이 지정되는 순간, 해당 루프처리까지 탈출하면서 후반부의 처리가 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
            var task = Task.Run(() =>
            {
                while (fm2 != null)
                {
                    System.Threading.Thread.Sleep(100);
                }
 
                string a = "";
 
                MessageBox.Show("1");
                MessageBox.Show("2");
                MessageBox.Show("3");
                
                if (a == string.Empty)
                {
                    return 0;
                }
                else
                {
                    return 1;
                }
            });
cs

위의소스에서 while 부분이 탈출된다는점이다.
그리고 메세지박스로 1, 메세지박스로 2, 메세지박스로 3 이 순서대로 출력되고,
마지막으로 리턴값이 0으로 돌아오게된다.


버튼4에서 fm2 를 종료하는 처리를 하게해 두었으니, 실행순서는 아래와같이 되겠다.

1. 샘플 프로그램을 실행한다.

2. 버튼2를 클릭한다.

Form2_Load가 출력

Form2_Shown가 출력

9가출력 <- Task.Run의 결과는 돌아오지 않았으므로, 후속처리는 그대로 실행되었기에 9가 돌아온다.

그리고, 여기서 버튼4를 누른순간, 아래와 같은 결과가 출력된다.

Form2_FormClosing가 출력

Form2_FormClosed가 출력

1 이 출력

2 가 출력

3 이 출력

- 끝 -


위의 소스를 이용해서 ShowDialog 로 되어있던 폼을 Show 로 불러오면서, 해당결과를 그대로 얻기위한 방법을 연구해보았다.

혹시나 같은 문제에 봉착한 사람들이 있다면, 조그마한 도움이라도 되고자 이 글을 남겨둔다.


소스는 아래의 링크를 참고하세요.

https://github.com/sungmanko/ApcEventControl

+ Recent posts