개발팁2022. 7. 27. 15:56

타자게임이란 키보드 타자 연습을 위한 게임으로,

한라인의 문장을 제시해주고, 똑같이 입력하면 성공, 그렇지 못하면 실패로 체크하고,

성공하면, 분당 타수를 알려주는, 간단한 게임을 말한다.

 

개인적으로 혼자서 연습도 가능하지만, 이를 채팅방에 적용하면, 다수의 사람과 경쟁을 통해 성취감을 얻을 수도 있다.

 

타자게임을 만들때 필요한 것은,

 

1. 한글 한줄 문장 데이터이다.

2. 한글을 초/중/종 성으로 쪼개는 작업이 필요하다.

3. 분당 타수를 계산하는 로직을 적용하면 된다.

 

 

 

 

 

프로그램은 시작 버튼을 누르면, 제시어를 보여주고, 똑같이 입력을 하면, 결과를 보여준다.

 

 

 

1. 한글 한줄 문장 데이터이다.

 

문장 데이터는 많을수록 좋다.

 

 

한컴 타자연습 사이트 - 짧은글 연습에서 문장을 가져왔다.

 


2692개의 문장이 제공되고 있다.

 

해당 문장은 DB 에 입력해서 꺼내와도 되고, 구글시트에 넣어놓고 꺼내와도 상관없다.

 

예제는 구글시트에 한줄 문장을 관리하기 때문에, 구글API 가 필요하다.

 

nuget 패키지 관리자에서, Google.Apis.Sheets.v4 를 설치한다.

 

 

 

public static List<string> GetTypingSentenceList()
{
List<string> result;

string _apikey = "***********************";
string _sheetid = "***********************";

result = new List<string>();

try
{
// API 서비스 생성
var service = new SheetsService(new BaseClientService.Initializer()
{
ApplicationName = "GoogleDocs .net API",
ApiKey = _apikey
});

var request = service.Spreadsheets.Values.Get(_sheetid, "타자!A1:A");

ValueRange response = request.Execute();
IList<IList<Object>> values = response.Values;
if (values != null && values.Count > 0)
{
foreach (var row in values)
{
if (row[0] != null && row[0].ToString() != "")
result.Add(row[0].ToString());
}
}

}
catch (Exception e1)
{
MessageBox.Show(e1.Message);
}

return result;
}

 

전체 한줄문장 목록은 이렇게 google sheet 에서 읽어오면 된다.

 

이렇게 읽어온 데이터는 캐시에 저장해두었다가, 요청시마다 꺼내서, 랜덤하게 한 라인을 select 하면 된다.

 

 

시작버튼 클릭시 처리 로직

 

Random rand = new Random();

// 문장 뽑기
List<string> sentence_list = GoogleDocsRepository.GetTypingSentenceList();
int rndNum = rand.Next(0, sentence_list.Count);
string sentence = sentence_list[rndNum].Trim();

this.txtKeyword.Text = sentence;

// 타자수 계산 : 1. 글자를 초/중/종 성으로 쪼갠다.
string[] split_words = HangulWorker.GetDisassemble(sentence, true);
// 타자수 계산 : 2. 글자수를 계산한다.
this._typingCount = HangulWorker.GetTypingSize(split_words);

this._isStart = true;
this._isKeyKirstPress = false;
this.txtInput.Focus();

 

이제 TextBox 에 첫 글자가 입력되는 순간부터 시간을 재고, 단어 입력이 끝나면, 결과를 발표하면 된다.

 

 

2. 한글을 초/중/종 성으로 쪼개는 작업이 필요하다.

 

제시어에서 글자를 모두 초/중/종 성으로 쪼개서, 타자수를 계산해야한다.

한글이라고 모두 2번의 타자가 아니기 때문이다.

 

안녕하세요 를 초/중/종 성으로 쪼개면

ㅇ ㅏ ㄴ ㄴ ㅕ ㅇ ㅎ ㅏ ㅅ ㅔ ㅇ ㅇ

12글자가 되는 것이다.

 

이중에 된발음 ㄲ, ㄸ 이런 글자는 shift 키를 누르고 글짜를 누르기 때문에, 두번의 타자로 계산할지 정의가 필요하다.

예제에서는 한번의 타자로 계산했다.

 

또한 종성에서는

갉 갏 값  등으로 ㄺ ㅀ ㅄ 등의 글자들이 있는데, 이 종성 글자들은 타자수 두번으로 계산해야 한다.

 

시작버튼 클릭시 처리 로직에서 사용한 HangulWorker 의 기본 코드

 

// 유니코드표에서의 기본 위치
private const int hangulStartingPoint = 0xAC00;  // hangulStartingPoint : 유니코드에서 '가'의 위치

// 자소 목록
private static List<string> chosungHangulWords = (
new string[] { "ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ"
 , "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ" }
).ToList<string>();    // 초성 19개 요소
private static List<string> jungsungHangulWords = (
   new string[] { "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ"
 , "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ"
 , "ㅣ" }
   ).ToList<string>();    // 중성 21개 요소
private static List<string> jongsungHangulWords = (
   new string[] { string.Empty, "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ"
 , "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ"
 , "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ" }
   ).ToList<string>();    // 종성 28개 요소

 

아래는, 한글문장을 초/중/종 성의 글자별로 쪼개는 GetDisassemble 함수 본문이다.

 

public static string[] GetDisassemble(string str, bool bSkipBase)
{
if (str == "") return null;

List<string> list = new List<string>();
int uniValue;   // 유니코드 상의 글자 위치
int iTemp;
string s;

for (int n = 0; n < str.Length; n++)
{
uniValue = 0;
s = str.Substring(n, 1);
byte[] bytes = Encoding.Unicode.GetBytes(s);
// 빅앤디안 방식이지만 아래 계산 편이를 위해 리틀앤디안으로 변환


// 유니코드 상의 글자 위치 구하기
for (int b = 0; b < bytes.Length; b++)
{
uniValue += (int)(bytes[b] * Math.Pow(256.0, b));
}

if (uniValue < hangulStartingPoint) { list.Add(s); continue; }

// 초성
list.Add(chosungHangulWords[(uniValue - hangulStartingPoint) / jongsungMultiply]);


// 중성
list.Add(jungsungHangulWords[(uniValue - hangulStartingPoint) % jongsungMultiply / jongsungHangulWords.Count]);

// 종성
iTemp = (uniValue - hangulStartingPoint) % jongsungHangulWords.Count;
if (bSkipBase && iTemp == 0) continue;
list.Add(jongsungHangulWords[iTemp]);
}

return list.ToArray();
}

 

GetDisassemble  에서 쪼개진 글자 배열에서 타자수를 계산하는 GetTypingSize 함수 본문이다.

 

public static int GetTypingSize(string[] str_list)
{
int addCount = str_list.Select(d => d == "ㅘ" || d == "ㅙ" || d == "ㅚ" || d == "ㅝ" || d == "ㅞ" || d == "ㅟ" || d == "ㅢ" || d == "ㄳ" || d == "ㄵ" || d == "ㄶ" || d == "ㄺ" || d == "ㄻ" || d == "ㄼ" || d == "ㄽ" || d == "ㄾ" || d == "ㄾ" || d == "ㅀ" || d == "ㅄ").Count(d => d);

return str_list.Length + addCount;
}

 

초/중/종 성을 모두 1글자로 취급하며,

ㅟ ㅚ ㄻ ㄼ  등의 글자는 2글자로 취급하도록 계산하였다.

 

 

3. 분당 타수를 계산하는 로직을 적용하면 된다.

 

이제, 입력글자가 모두 일치 할경우, 분당 타수를 계산해서 알려주면 끝이다.

 

입력 TextBox 의 KeyDown 이벤트를 아래와 같이 정의한다.

 

private void txtInput_KeyDown(object sender, KeyEventArgs e)
{
if (this._isStart == true)
{
if (this._isKeyKirstPress == false)
{
this._isKeyKirstPress = true;
this._startDateTime = System.DateTime.Now;
}

if (this.txtKeyword.Text == this.txtInput.Text)
{
this._isStart = false;
this.checkDone();
}
}
}

 

이제 분당 타수를 표시해주면 끝이난다.

 

private void checkDone()
{
TimeSpan elapsed = DateTime.Now - this._startDateTime;

double typingMM = Math.Round(_typingCount * (60000.0 / elapsed.TotalMilliseconds));

this.txtResult.Text = $"{typingMM:N0} 타/분";
}

 

 

코드를 모두 적용하고, 프로그램을 실행하면, 아래와 같은 결과가 나오게 된다.

 

 

 

 

이것은 혼자하는 타자게임이지만, 채팅방에 이 로직을 적용하면, 여러명이서 경쟁하면서 게임을 즐길 수 있다.

 

아래는, 실제 네이버웍스메신저 단톡방에 메세지Bot 으로 구현한 타자 게임이다.

 

 

 

 

직접 만든 채팅방에 적용할 수 있다면, 좀더 편리한 UI 로 구현할 수 있을 것이다.

 

 

Posted by 헝개
개발팁2022. 4. 7. 11:53

PC에 이더넷 어댑터가 여러개 설치되어 있거나, Virtual Box 와 같은 Virtual Marchine 이 설치되어 있다면,

IP주소 / 맥어드레스를 구하는 다른 소스로는 엉뚱한 IP/Mac 주소를 반환하는 경우가 있다.

정확한 사용 주소를 가져오는 C# 코드는 아래와 같다.

 

 

IP주소

 

            string ipAddress = "";

            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0))
            {
                socket.Connect("8.8.8.8", 65530);
                IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint;
                ipAddress = endPoint.Address.ToString();
            }

 

 

맥어드레스

 

            string macAddress = "";

            ManagementClass mc = new ManagementClass("Win32_NetworkAdapterConfiguration");
            using (ManagementObjectCollection moc = mc.GetInstances())
            {
                foreach (ManagementObject mo in moc)
                {
                    if (mo["MacAddress"] != null)
                    {
                        if ((bool)mo["IPEnabled"] == true)
                        {
                            macAddress = mo["MacAddress"].ToString();
                            break;
                        }

                        mo.Dispose();
                    }
                }

            }
Posted by 헝개
개발팁2022. 3. 22. 18:46

웹API 및 Rest API 에서 swagger 는 강력한 테스트 도구이자, 문서화 도구이며,

API 를 사용하는 개발자와의 인터페이스 도구이다.

 

 

Swagger 설치 및 환경 설정

 

 

 

NuGet 패키지관리자에서 swashbuckle 을 검색해서 최신버전을 설치한다.

 

 

 

프로젝트 설정에서

웹 > 시작 작업 > 특정 페이지

 

swagger/ui/index

 

를 입력한다.

 

 

이제 F5 를 눌러 디버그를 해보면, 바로 swagger 페이지가 호출된다.

 

 

REST API 의 기본 샘플인 Values 컨트롤러에 대한 API 테스트 화면이다.

 

API 테스트 도구로서는 훌륭하지만, 문서화로써는 부족하다.

 

 

 

프로젝트 설정 > 빌드 > 출력

XML 문서 파일 체크를 한다.

 

 

 

 

 

 

App_Start > SwaggerConfig.cs 파일을 편집한다.

 

EnableSwagger 아래에 아래 라인을 추가한다.

 

c.IncludeXmlComments(string.Format(@"{0}\bin\WebApiTest.xml",
       System.AppDomain.CurrentDomain.BaseDirectory));

 

그리고, 테스트용 Api 컨트롤러 TodoController.cs 를 생성한다.

 

    public class TodoController : ApiController
    {
        // GET api/Todo
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/Todo/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/Todo
        /// <summary>
        /// Test 데이터 업로드 (dynamic by json)
        /// </summary>
        /// <remarks>
        /// {<br />
        ///     UserID: "round1",<br />
        ///     UserName: "홍길동",<br />
        ///     WorkTitle: "데이터 백업"<br />
        /// }<br />
        /// </remarks>
        /// <param name="data">
        /// json object {<br />
        /// UserID: 유저아이디<br />
        /// UserName: 유저이름<br />
        /// WorkTitle: 작업이름<br />
        /// }<br />
        /// </param>
        /// <returns></returns>
        public string Post([FromBody] dynamic data)
        {
            return "Data Posted : " + data.UserID + "=" + data.UserName + "<br />" + data.WorkTitle;
        }


        // PUT api/Todo/5
        /// <summary>
        /// PUT 데이터
        /// </summary>
        /// <remarks>
        /// {<br />
        ///     UserID: "round1",<br />
        ///     UserName: "홍길동",<br />
        ///     WorkTitle: "데이터 백업"<br />
        /// }<br />
        /// </remarks>
        /// <param name="data">
        /// json object {<br />
        /// UserID: 유저아이디<br />
        /// UserName: 유저이름<br />
        /// WorkTitle: 작업이름<br />
        /// }<br />
        /// </param>
        public string Put(int id, [FromBody] dynamic data)
        {
            return "put : " + data.WorkTitle;
        }

        // DELETE api/Todo/5
        public void Delete(int id)
        {
        }
    }

 

샘플데이터로 ValuesController 를 복사해서 수정했다.

Post / Put 메서드에 xml 주석이 들어가 있는것을 볼 수 있다.

 

프로젝트를 빌드 하면, 이 주석이 bin/WebApiTest.xml 파일로 저장이 되고,

Swagger 에서는 의 xml 을 읽어서 화면을 만들어 주게 된다.

 

 

 

TodoController 에서 메서드에 작성한 xml 주석이 swagger 에 잘 나온다.

 

xml 주석에 작성해둔 json 샘플 양식도 복사해서 붙여 넣기 하고,

Try it out! 을 누르면, 테스트도 간편하게 된다.

 

 

여기서 한가지 더 개선을 해본다면,

 

 

매번 json 샘플을 복사해서, parametar 에 붙여넣기를 해야 하는데, 이걸 자동화 해볼 것이다.

 

 

 


// Copyright 헝그리개발자(https://bemeal2.tistory.com)
// 소스는 자유롭게 사용가능합니다. Copyright 는 삭제하지 마세요.

$(document).ready(function () {

$(".toggleOperation").on('click', function (e) {

let el_content = $(e.target).closest("li.operation").find(".content");

setTimeout(function () {
if (el_content.css("display") == "block") {

let param_text = el_content.children(".markdown").text();

let el_textarea = el_content.find("textarea.body-textarea");

el_textarea.val(param_text);
let ev = new Event('input', { bubbles: true });
ev.simulated = true;

el_textarea[0].dispatchEvent(ev);

}
}, 500);

});


});

 

js 폴더를 만들고 그 아래  swager-control.js 파일을 생성한다.

 

.js 파일의 내용은 위와 같이 넣어준다.

 

 

그리고, 속성창에서 빌드작업을 [포함리소스] 로 바꿔준다.

 

SwaggerConfig.cs 파일을 열어서, EnableSwaggerUI 아래에 아래와 같이 추가해준다.

 

                .EnableSwaggerUi(c =>
                    {
                        c.InjectJavaScript(thisAssembly, "WebApiTest.js.swagger-control.js");

 

여기서 주의 할 점은 .js 파일의 경로를 지정하는 방식이다.

 

프로젝트명.폴더명.파일명  이러한 규칙으로 작성해야 한다.

 

상식적으로는 js 폴더 아래에  swagger-control.js  파일이 있으니까

"/js/swagger-control.js"  라고 해야 할것 같지만. 그렇지 않다.

 

WebApiTest 라는 프로젝트명.js라는폴더명.swagger-control.js라는파일명

 

이렇게 해서, 

"WebApiTest.js.swagger-control.js"

 

이러한 양식으로 입력해줘야 한다.

 

 

이제 swagger 페이지를 열어보면, 파라미터가 자동으로 입력된것을 볼 수 있다.

API 테스트를 위해서 Try it out! 만 눌러주면 된다!!!

 

Posted by 헝개
개발팁2019. 3. 28. 14:41

ToggleButton 은 버튼의 눌림을 표현 하는 방법으로,

한번 누르면 눌림상태로, 다시 누르면, 튀어 나온 상태를 말한다.


[Winform]

 

System.Windows.Forms 에는 별도의 ToggleButton 이 없고, 일반 Button 은 ToggleButton 처럼 사용할 수 없다.

 

System.Windows.Forms 의 CheckBox 나 RadioButton 의 Appearance 속성을 이용하여, ToggleButton 으로 사용할 수 있다.

 

일단, Panel 위에 CheckBox 나 RadioButton 을 올려놓는다.

 

한 Container 위에서, CheckBox 는 여러개 선택이 가능하고, RadioButton 은 단 1개만 선택이 가능하다.

 

 

CheckBox 나 RadioButton Control의 속성을 보면,

Appearance 의 기본값 Normal 을 Button 으로 바꿔준다.

 

 

이제 버튼의 모양도 동일하게 보여지게 된다.

 

 

System.Windows.Forms
CheckBox

버튼의 눌림은 .Checked = true, 눌림해제는 .Checked = false 로 체크박스나 라디오버튼으로 그대로 사용하며,

화면에 보여지는 형식만 다르다는것을 알 수 있다.

 

 

[WPF]

 

WPF 에는 ToggleButton 컨트롤이 있어서, 바로 사용이 가능하다.

 

<ToggleButton Height="25" Content="Level 1" IsChecked="{Binding level1_checked, UpdateSourceTrigger=PropertyChanged}" Checked="ShowScore"></ToggleButton>

 

Winform 에서 Checkbox 로 토글버튼을 만든것과 동일한 결과를 얻을수 있다.

 

추가적으로, RadioButton 으로 컨테이너 또는 그룹 안에서 하나의 토글 버튼만 선택이 가능하게 하기 위해서는,

Winform 처럼 RadioButton 으로 구현하면 된다.

 

WPF 의 RadioButton 컨트롤의 Style 속성을 지정하여, 토글버튼으로 보여지게 된다.

 

Style="{StaticResource {x:Type ToggleButton}}"

 

<RadioButton Height="25" Content="Level 1" Style="{StaticResource {x:Type ToggleButton}}" IsChecked="{Binding level1_checked, UpdateSourceTrigger=PropertyChanged}"></RadioButton>
<RadioButton Height="25" Content="Level 2" Style="{StaticResource {x:Type ToggleButton}}" IsChecked="{Binding level2_checked, UpdateSourceTrigger=PropertyChanged}"></RadioButton>
<RadioButton Height="25" Content="Level 3" Style="{StaticResource {x:Type ToggleButton}}" IsChecked="{Binding level3_checked, UpdateSourceTrigger=PropertyChanged}"></RadioButton>
<RadioButton Height="25" Content="Level 4" Style="{StaticResource {x:Type ToggleButton}}" IsChecked="{Binding level4_checked, UpdateSourceTrigger=PropertyChanged}"></RadioButton>
<RadioButton Height="25" Content="Level 5" Style="{StaticResource {x:Type ToggleButton}}" IsChecked="{Binding level5_checked, UpdateSourceTrigger=PropertyChanged}"></RadioButton>

 

WPF RadioButton 을 이용한 토글버튼

 

Posted by 헝개
개발팁2019. 3. 19. 14:42

[C#] 파일시스템의 폴더감시 FileSystemWatcher

 

System.IO.FileSystemWatcher

 

System.IO 에 속한, FileSystemWatcher 를 이용하면, 폴더내의 파일시스템의 변화를 감지할 수 있다.

 

여기서 말하는 파일 시스템은, 파일과 폴더를 모두 포함한다.

 

감지할 수 있는 이벤트는

 

Changed - 파일이나 폴더의 변경
Created - 파일이나 폴더의 생성
Deleted - 파일이나 폴더의 삭제
Renamed - 파일이나 폴더의 이름 변경

 


실제 코드는 아래와 같이 구성하면 된다.

 

 

private void Run_Watcher()
{
 this.m_FolderWatcher = new FileSystemWatcher();

 this.m_FolderWatcher.Filter = "*.*";
 this.m_FolderWatcher.Path = "C:\\Program Files\\";
 this.m_FolderWatcher.IncludeSubdirectories = true;

 this.m_FolderWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
 this.m_FolderWatcher.Changed += new FileSystemEventHandler(Watcher_OnChanged);
 this.m_FolderWatcher.Created += new FileSystemEventHandler(Watcher_OnChanged);
 this.m_FolderWatcher.Deleted += new FileSystemEventHandler(Watcher_OnChanged);
 this.m_FolderWatcher.Renamed += new RenamedEventHandler(Watcher_OnRenamed);
 this.m_FolderWatcher.EnableRaisingEvents = true;
}

 

private void Watcher_OnChanged(object sender, FileSystemEventArgs e)
{
 Console.WriteLine(string.Format("{0} {1}", e.FullPath, e.ChangeType.ToString()));
}

 

private void Watcher_OnRenamed(object sender, RenamedEventArgs e)
{
 Console.WriteLine(string.Format("{0} {1} to {2}", e.OldFullPath, e.ChangeType.ToString(), e.Name));
}

 

 

 

Filter 라는것은, 지정된 경로(Path) 아래에서도, 모든 파일(*.*) 을 다 감시 할지, 특정 파일(ex : *.txt) 만 감시할지를 지정하게 된다.

 

IncludeSubdirectories 는 경로(Path) 아래의 모든 하위폴더를 다 포함할지 여부이다.

 

NotifyFilter 는 파일이나 폴더의 변경형식을 선택할 수 있다.

 

다수의 형식을 선택하기 위해 bitwise or 연산자를 사용한다.

8가지중에 선택할 수 있다.

 

FileName = 1,
DirectoryName = 2,
Attributes = 4,
Size = 8,
LastWrite = 16,
LastAccess = 32,
CreationTime = 64,
Security = 256

 

 

Changed, Created, Deleted 는 모두 event 파라미터가 동일(FileSystemEventArgs) 하기 때문에, 하나의 이벤트 핸들러로 연결시켰다.
(각자 별도의 이벤트 핸들러를 만들어도 상관은 없다.)


FileSystemEventArgs 의 ChangeType 으로, 어떤 변화인지 찾아 낼 수 있다. (Created, Changed, Deleted, Renamed)

 

마지막으로,
EnableRaisingEvents = true

 

를 해줌으로써, 파일시스템의 감시가 활성화 된다.

 

감시를 중단하고 싶을때는,

EnableRaisingEvents = false

 

로 해주면 된다.

실제로, 특정 폴더를 감시하는 프로그램은 아래와 같이 나오게 된다.

 

 

 

 

한가지 팁으로,

 

특정 FTP 경로를 감시하고 있다가, FTP 로 업로드가 될 경우, 자동으로 배포 하는 프로그램을 만든다면,

어떤 이벤트를 사용해야 할까?

Created? Changed?

 

정답은 2개를 다 사용해야 한다.

 

우선, FTP 로 파일이 업로드 시작되면, Created 이벤트가 발생한다.

그리고, 파일이 업로드가 진행되면, Changed 가 발생하게 된다.

 

여기서 중요한것은 FTP로 파일 업로드가 종료된(파일이 모두 업로드된) 시점을 알아야 한다.

이를 위해, 파일을 Write 모드로 Open 해서, Open 이 된다면, 정상적으로 업로드가 완료된것으로 보면 된다.

 

 
private void Watcher_OnCreated(object sender, FileSystemEventArgs e)
{
 string ext = Path.GetExtension(e.FullPath);

 this.m_Wacher_CreatedFileName = e.FullPath;
}

 

private void Watcher_OnChanged(object sender, FileSystemEventArgs e)
{
 if (e.ChangeType != WatcherChangeTypes.Changed)
  return;

 if (this.m_Wacher_CreatedFileName != e.FullPath)
  return;

 

 // 쓰기모드로 오픈이 가능해야, 파일복사가 완료된것으로 본다.
 try
 {
  FileStream fs = File.Open(e.FullPath, FileMode.Open);
  fs.Close();
  fs.Dispose();
 }
 catch(IOException)
 {
  return;
 }
}

 

 

위 코드는 실제로 사용되는 코드이다.

 

 

Posted by 헝개
개발팁2017. 12. 18. 13:10

C# 에서 파일을 복사하는 방법은

 

간단하게,

 

System.IO.File.Copy 메서드를 이용하는 방법이다.

 

System.IO.File.Copy( source_file_nm, target_file_nm, is_overwrite);

 

단순하면서도, 적절한 Exception 처리만 해준다면, 아주 강력하다.

 

하지만, 한줄의 명령어로, Synchronous 작업이기 때문에, 파일이 복사 되는 동안에는 아무것도 하지 못한다.

 

파일이 복사되는 동안에, 진행률을 표시하기 위해서는, 위의 파일 복사 방법이 아닌, 비동기 작업 또는 쓰레드 작업이 필요하다.

 

파일이 복사 되는 동안에, 진행률도 표시해야 하고, 프로그레스바도 움직여줘야 하기 때문이다.

 

위의 방법은 한번에 파일복사 명령을 주고, 진행이 끝나면, 알려주는 방법이라면,

 

아래의 방법은, 우리가 직접 파일 복사를 주도하게 될것이다.

 

복사할 source_file_nm 의 경로에서 파일을 조금씩(buffer) 읽어서,  target_file_nm 에다가 write 해주는 방법이며,

 

이것은, loop 를 통해서, 파일의 내용을 모두 복사할때까지, 반복으로 작업을 진행하며,

 

진행하는 중간에 진행률과, 프로그레스바를 같이 움직여 줄것이다.

 

        byte[] buf = new byte[1024 * 512];  // 0.5M

 

        FileInfo file = new FileInfo(source_file_nm);

 

        if (!Directory.Exists(target_path))
                Directory.CreateDirectory(target_path);

 

        FileStream strIn = new FileStream(source_file_nm, FileMode.Open);
        FileStream strOut = new FileStream(Path.Combine(target_path, target_file_nm), FileMode.Create); 

 

 

버퍼를 0.5M 로 했으나, 1M로 해도 상관이 없다.

 

source 파일을 읽기용으로 Open 하고, target 파일을 쓰기용으로 Open 했다.

 

                while (strIn.Position < strIn.Length)
                {
                        int len = strIn.Read(buf, 0, buf.Length);
                        strOut.Write(buf, 0, len);
                }

 

                strOut.Flush();

                strIn.Close();
                strOut.Close();

 

source 를 버퍼로 (0.5M씩) 읽어서, target 파일에다 쓰기를 하고, 완료가 되면, 스트림을 모두 닫아준다.

 

이렇게 하면, 버퍼로 읽어서, 파일에 쓰는 작업은 완료가 되었다.

 

이제, 작업 중간에, 진행률과 프로그레스 바를 움직일 것이다.

 

진행률 표시를 위해 Label 을 하나 만들고, ProgressBar 를 하나 만들어 준다.

 

그리고, 아래와 같이 미리 설정을 해둔다.

 

progressBar.Maximum = Int32.MaxValue;


 

        byte[] buf = new byte[1024 * 512];  // 0.5M

        FileInfo file = new FileInfo(source_file_nm);

 

        if (!Directory.Exists(target_path))
                Directory.CreateDirectory(target_path);

 

        FileStream strIn = new FileStream(source_file_nm, FileMode.Open);
        FileStream strOut = new FileStream(Path.Combine(target_path, target_file_nm), FileMode.Create);

 

        try
        {
                while (strIn.Position < strIn.Length)
                {
                        int len = strIn.Read(buf, 0, buf.Length);
                        strOut.Write(buf, 0, len);

 

                        progressBar.Value = (int)(Int32.MaxValue * strIn.Position / strIn.Length);

                        this.lblBytes.Text = string.Format("{0:N0} / {1:N0}", strIn.Position, strIn.Length);
                }
        }
        catch (Exception ex)
        {
                MessageBox.Show("Error Occured : " + ex.Message, "Error");
                this.DialogResult = System.Windows.Forms.DialogResult.Abort;
        }
        finally
        {
                strOut.Flush();

                strIn.Close();
                strOut.Close();
        }

 

 

이렇게 버퍼로 복사하는 중간에 진행률과 프로그레스바를 표시하도록 수정했다.

그러나, 실제 파일이 복사되는 과정에서, 복사과 완료될동안, 응답없음 상태로 나왔다가, 파일 복사가 끝나야 결과가 나올 것이다.

 

위 작업을 쓰레드로 처리하거나, 백그라운드워커로 실행해줘야 한다.

 

BackgroundWorker 객체를 하나 만들고, 아래와 같이 이벤트 처리를 해준다.

 

this.bg_filecopy = new BackgroundWorker();
this.bg_filecopy.WorkerSupportsCancellation = true;
this.bg_filecopy.DoWork += new DoWorkEventHandler(bg_filecopy_DoWork);
this.bg_filecopy.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bg_filecopy_RunWorkerCompleted);

위에서 만들어준 코드는 bg_filecopy_DoWork 메서드 안에 넣어주면 된다.

 

이로써, 파일이 복사되는 중간에 상태표시가 되는것을 확인할 수 있다.

하지만, 뭔가 이상한 점이 있다.

 

위에서 만들어준 byte 버퍼를 사용하였지만, 실제 시스템에서는 또다른 버퍼가 있다는 것이다.

 

위에서 마지막 Flush 는 버퍼에 있는것을 모두 파일로 쓸 것이다.

하지만, Loop 안에 strOut.Write(buf, 0, len); 가 있지만, 시스템은 계속 버퍼에 쌓아두었다가 일정 시점 이후에 쓰기를 하는 것을 볼 수 있다.

 

따라서, Loop 안에서도 일정 시점마다  Flush 를 하도록 수정하는것이 좋다.

 

 

 

 

 

Posted by 헝개
개발팁2017. 11. 1. 14:39

C# WinForm 의 GroupBox 컨트롤의 title 영역에는 text 가 표시가 되지만,

 

이부분에 Checkbox 를 넣고, 체크박스가 선택된 경우에 GroupBox 가 활성화 되도록 하는 방법이다.

 

Form 안에 GroupBox 와 CheckBox 컨트를을 각각 1개씩 생성한다.

 

GroupBox 의 Text 값은 비워둔다.

CheckBox 의 Text 값은 적당한 값으로 적어준다.

 

CheckBox 의 위치는 아무곳에나 두어도 상관이 없다.

 

CheckBox Control 에서 마우스 오른쪽 버튼을 눌러서,

 

맨앞으로 가져오기 를 한번 클릭해준다.

 

그래야, GroupBox 에 CheckBox 가 감춰지지 않고 잘 보여지게 된다.

 

그리고, FORM 의 생성자를 아래와 같이 정의하자

 

public CustomForm()
{

    InitializeComponent();


    // 이부분을 추가한다.
    this.chkImageChange.Location = new Point(this.group_image.Location.X + 13, this.group_image.Location.Y - 1);
    this.chkVideoChange.Location = new Point(this.group_video.Location.X + 13, this.group_video.Location.Y - 1);

 

이부분은, CheckBox 컨트롤의 위치를 GroupBox컨트를의 title 영역위에 위치하도록 해주는 부분다.

 

이제 CheckBox 컨트롤의 이벤트를 추가할 것이다.

 

CheckedChanged 이벤트를 아래와 같이 정의한다.

 

private void chkImageChange_CheckedChanged(object sender, EventArgs e)
{
    this.group_image.Enabled = this.chkImageChange.Checked;

 

CheckBox 컨트롤을 체크하면, 그룹박스가 활성화되고, 해제되면, 그룹박스도 비활성화 될것이다.

 

이제, Form 에서 기본값으로 활성화 할지, 비활성화 할지를 선택하면 된다.

 


실행 화면 : 아래와 같은 형태로 나올 것이다.


 

 

 

Posted by 헝개
개발팁2017. 10. 28. 17:14

C1FlexGrid 를 새로 만들면, 기본적으로 1번째 column 이 회색으로 나온다.

이 부분을 fixed 라고 부르는데, 이 부분에 행번호(Line Number)를 표시하는 방법이다.

 

 

 

Caption : No 라고 적고,

DataType : Int32 로 변경한다.

Name : 은 비워둔다.

 

물론 Data Source 에 행번호를 넣어서 바인딩 할 수도 있지만, 이렇게 되면, Sort, Filtering 하게 되면, 행번호가 꼬이게 된다.

 

데이터로써가 아니라 항상 위에서 1부터 시작하는 행번호를 넣을 것이다.

 

 

 

 

그리드의 속성창에서 DrawMode 를 Normal -> OwnerDraw 로 변경한다.

 

 

 

이제, 그리드의 OwnerDrawCell 이벤트를 아래와 같이 정의한다.

 

private void FlexGrid_OwnerDrawCell(object sender, OwnerDrawCellEventArgs e)
{
 if ((e.Row >= this.Rows.Fixed) && (e.Col == (this.Cols.Fixed - 1)))
 {
     e.Text = ((e.Row - this.Rows.Fixed) + 1).ToString();
 }
}

 

이제, 빌드 해서 실행해보면 행번호가 잘 나올것이다.

 

 

[Sample]

 

 

Posted by 헝개
개발팁2016. 8. 26. 10:46

 

MS-SQL 에 보면, COALESCE 라는 함수가 있다.

(근데, 이거 어떻게 발음하는건가요? 단어가 굉장히 어려워 보이는데..;;)

 

SELECT COALESCE(@a, @b, @c, 'else');

 

앞에서 순서대로 null 이 아닌 값을 찾아오는 함수다.

 

C# 에서도 비슷한 기능을 하는 연산자가 있다.

 

? 라는 연산자는 3항 연산자로 쓰이는데,

 

a == b ? 'same' : 'not same';

 

물음표를 2개 써서,   value1 ?? value2

 

앞에서 순서대로 null 이 아닌 값을 찾는다.

 

물론, 2개 이상의 값에서 찾는다면 , 순서대로 뒤에 ?? 를 나열하면 된다.

 

int? a = null;
int? b = null;
int? c = 100;

int? k = a ?? b ?? c;

int? 는 int 가 primitive type 이기 때문에 null 값이 들어갈 수 없다. int? 는 null 값을 허용한다는 뜻이다.

 

a, b, c 중에 앞에서 순서대로 null 이 아닌 값이 k 에 할당된다.

 

 

if(strV1 != null)

{

    System.out.println("the value is " + strV1);

}

else if(strV2 != null)

{

    System.out.println("the value is " + strV2);

}

else

{

    System.out.println("the value is else");

}

 

if / else 로 구성된 문장은 3항 연산자로 아래와 같이 쓸 수 있다.

 

System.out.println("the value is " + ( strV1 != null ? strV1 : strV2 != null ? strV2 : 'else' ) );

 

이는 다시 ?? 연산자로 아래와 같이 사용이 가능하다.

 

System.out.println("the value is " + ( strV1 ?? strV2 ?? 'else' ) );

 

 

Posted by 헝개
개발팁2016. 8. 10. 14:55

C# 에서 string 타입으로 숫자가 저장되어 있을때 int 타입으로 변경하는 몇가지 방법이 있다.

 

string n = "1234";

 

라고 할때,

 

 

Method.1

 

int outValue = Convert.ToInt32(n);

 

 

Method.2

 

int outValue = int.Parse(n);

 

 

Method.3

 

int outValue;

int.TryParse(n, out outValue);

 

 

그리고, 여기서 소개할 방법은, C# 의 기본 메서드를 이용하지 않고, 자체적으로 제작을 해보겠다.

 

문자로 저장된 '0' 이라는 값은 실제 메모리상에는 코드값으로 저장이 되어 있다.

우리가 ASCII 코드라고 부르는 값이다.

실제로 '0' 은 48 이라는 값으로 저장이 되며, '9' 는 57 이라는 값으로 저장이 된다.

 

하지만, '0' ~ '9' 가 어떤 값으로 저장이 되는지 알필요도 없다.

 

'0' - '0' 은 0 이 되며,

'1' - '0' = 1 이 되고,

'9' - '0' = 9 가 된다.

 

 

string n = '1234';

int outValue = 0;

 

for (int i = 0; i < n.Length; i++)
{

    outValue = outValue * 10 + (n[i] - '0');

}

 

outValue 를 확인해보면, 1234 라는 값이 들어있는것을 확인할 수 있다.

 

이를 Method.4 라고 부르고,

 

 

Method.1 ~ Method.4 까지 퍼포먼스 테스트를 해봤다.

 

1,000,000 개의 4자리 문자열로 된 배열을 만들고,

 

이를 int 형으로 변환하는 4가지 방법을, 각각 20회씩 실행해 봤다.

 

 

 

 

Convert.ToInt32
int.Parse
int.TryParse
3가지 방법 모두, 비슷한 결과가 나왔다.

4자리 문자열 100만개를 변환하는데, 0.13 ~ 0.14 초가 걸렸다.

 

반면, 직접 만들어서 int로 변환한 경우는, 0.03, 0.04 초가 걸렸다. 속도면에서, 기본 메서드 보다, 4배이상 빨랐다.

 

Posted by 헝개