반응형

시간과 임의의 값을 비동기적으로 텍스트 파일로 저장하고 확인해 보자. 

 

 

namespace DateTimeValue
{
    public partial class Form1 : Form
    {
        private readonly string filePath = "output.txt";        
        readonly Random random = new Random();

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
            button2.Click += Button2_Click;
        }

        // 비동기적으로 파일에 현재 시간과 랜덤 값을 기록하는 버튼 클릭 이벤트 핸들러
        // 비동기 메서드는 Task를 반환하는게 좋지만 이벤트 핸들러는 void를 반환해야 하므로 어쩔 수 없이 async void를 사용
        private async void Button1_Click(object? sender, EventArgs e)
        {
            try
            {
                // 버튼 클릭 시 버튼을 비활성화하여 중복 클릭 방지
                button1.Enabled = false; // 버튼 비활성화

                FileStream fileStream = new FileStream (
                    filePath,
                    FileMode.Append,  // 파일이 없으면 새로 생성하고, 있으면 이어쓰기
                    FileAccess.Write, // 쓰기 전용
                    FileShare.Read); // 다른 프로세스가 파일을 읽을 수 있도록 허용, 내가 쓰는 동안 다른 프로세스는 읽기 가능
                
                using (StreamWriter writer = new StreamWriter(fileStream, System.Text.Encoding.UTF8))
                {
                    for (int i = 0; i < 5; i++)
                    {
                        await writer.WriteLineAsync(DateTime.Now.ToString("yyyyMMdd-HHmmss") + " " + random.Next());
                        // 비동기적으로 파일에 기록
                        await writer.FlushAsync();
                        // 비동기적으로 버퍼를 파일에 기록

                        // 비동기적으로 1초 대기
                        await Task.Delay(1000);
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"오류 발생: {ex.Message}");
            }
            finally
            {
                button1.Enabled = true; // 버튼 활성화
            }
        }

        private async void Button2_Click(object? sender, EventArgs e)
        {
            listBox1.Items.Clear(); // 기존 항목 제거
            try
            {
                FileStream fileStream = new FileStream(
                    filePath,
                    FileMode.Open, // 파일이 없으면 예외 발생
                    FileAccess.Read, // 읽기 전용
                    FileShare.ReadWrite); // 다른 프로세스가 파일을 읽을 수 있도록 허용, 내가 읽는 동안 다른 프로세스도 읽기/쓰기 가능

                using (StreamReader reader = new StreamReader(fileStream, System.Text.Encoding.UTF8))
                {
                    string? line;
                    while ((line = await reader.ReadLineAsync()) != null)
                    {
                        //listBox1.Items.Add(line);

                        // 공백 문자를 기준으로 분리
                        string[] parts = line.Split(' ');
                        if (parts.Length == 2)
                        {
                            listBox1.Items.Add("시간: " + DateTime.ParseExact(parts[0], "yyyyMMdd-HHmmss", null) + "\t값: " + parts[1]);
                            // ParseExact() 세번째 인자를 null로 주면 현재 문화권의 형식으로 해석, InvariantCulture를 주면 문화권에 상관없이 해석
                        }
                        else
                        {
                            listBox1.Items.Add("Error");
                        }
                    }
                }
            }
            catch (FileNotFoundException)
            {
                MessageBox.Show($"{filePath} 파일이 존재하지 않습니다.");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"오류 발생: {ex.Message}");
            }            
        }
    }
}

 

Record 버튼을 클릭해 로그 파일을 생성하고 Show 버튼을 클릭해 원하는 로그 파일을 선택하면 리스트 박스에 로그가 표시된다.

 

그래프를 추가해 보자.

 

Graph 버튼을 추가한다.

 

using System.Diagnostics;

namespace DateTimeValue
{
    public partial class Form1 : Form
    {
        internal string filePath = string.Empty; // Form2에서 접근할 수 있도록 internal로 선언
        readonly Random random = new Random();

        public Form1()
        {
            InitializeComponent();

            button1.Click += Button1_Click;
            button2.Click += Button2_Click;
            button3.Click += Button3_Click;
        }

        // 비동기적으로 파일에 현재 시간과 랜덤 값을 기록하는 버튼 클릭 이벤트 핸들러
        // 비동기 메서드는 Task를 반환하는게 좋지만 이벤트 핸들러는 void를 반환해야 하므로 어쩔 수 없이 async void를 사용
        private async void Button1_Click(object? sender, EventArgs e)
        {
            try
            {
                // 버튼 클릭 시 버튼을 비활성화하여 중복 클릭 방지
                button1.Enabled = false; // 버튼 비활성화

                int count = 120; // 테스트를 위해 120회(120초)만 기록
                string mm = string.Empty;
                string now = string.Empty;
                bool needNewFile = false; // 새로운 파일 생성 여부를 나타내는 플래그

                while (true)
                {
                    filePath = DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".txt"; // 파일 이름을 현재 시간으로 설정                    
                    mm = Path.GetFileName(filePath).Substring(11, 2); // 파일 이름에서 '분'만 추출

                    FileStream fileStream = new FileStream(
                        filePath,
                        FileMode.Append,  // 파일이 없으면 새로 생성하고, 있으면 이어쓰기
                        FileAccess.Write, // 쓰기 전용
                        FileShare.Read); // 다른 프로세스가 파일을 읽을 수 있도록 허용, 내가 쓰는 동안 다른 프로세스는 읽기 가능

                    using (StreamWriter writer = new StreamWriter(fileStream, System.Text.Encoding.UTF8))
                    {
                        while (true)
                        {
                            now = DateTime.Now.ToString("yyyyMMdd-HHmmss");
                            if (now.Substring(11, 2) != mm) // 현재 '분'이 파일 이름의 '분'과 다르면
                            {
                                needNewFile = true;
                                break; // 루프 종료하여 새로운 파일 생성
                            }

                            await writer.WriteLineAsync(now + " " + random.Next(1, 11)); // 1부터 10까지의 랜덤 값을 기록
                            // 비동기적으로 파일에 기록
                            await writer.FlushAsync();
                            // 비동기적으로 버퍼를 파일에 기록

                            await Task.Delay(1000);
                            // 비동기적으로 1초 대기

                            count--;
                            if (count < 0)
                                break; // 기록 횟수가 0이 되면 루프 종료
                            Debug.WriteLine($"남은 기록 횟수: {count}");
                        }
                    }

                    // needNewFile보다 count를 먼저 확인하여 무한 루프를 방지
                    if (count < 0)
                    {
                        break; // 무한 루프 종료
                    }
                    if (needNewFile)
                    {
                        needNewFile = false; // 새로운 파일 생성 여부 플래그 초기화
                        continue; // 새로운 파일 생성 루프 시작
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"오류 발생: {ex.Message}");
            }
            finally
            {
                button1.Enabled = true; // 버튼 활성화
            }
        }

        private async void Button2_Click(object? sender, EventArgs e)
        {
            FileDialog fileDialog = new OpenFileDialog();
            fileDialog.InitialDirectory = Application.StartupPath; // 실행 파일이 있는 폴더 경로
            fileDialog.Filter = "텍스트 파일 (*.txt)|*.txt|모든 파일 (*.*)|*.*";
            fileDialog.Title = "파일 선택";
            fileDialog.DefaultExt = "txt";

            if (fileDialog.ShowDialog() == DialogResult.OK)
                filePath = fileDialog.FileName;
            else
                return; // 파일 선택 취소 시 종료

            listBox1.Items.Clear(); // 기존 항목 제거

            try
            {
                FileStream fileStream = new FileStream(
                    filePath,
                    FileMode.Open, // 파일이 없으면 예외 발생
                    FileAccess.Read, // 읽기 전용
                    FileShare.ReadWrite); // 다른 프로세스가 파일을 읽을 수 있도록 허용, 내가 읽는 동안 다른 프로세스도 읽기/쓰기 가능

                using (StreamReader reader = new StreamReader(fileStream, System.Text.Encoding.UTF8))
                {
                    string? line;
                    while ((line = await reader.ReadLineAsync()) != null)
                    {
                        //listBox1.Items.Add(line);

                        // 공백 문자를 기준으로 문자열 분리
                        string[] parts = line.Split(' ');
                        if (parts.Length == 2)
                        {
                            listBox1.Items.Add("시간: " + DateTime.ParseExact(parts[0], "yyyyMMdd-HHmmss", null) + "\t값: " + parts[1]);
                            // ParseExact() 세번째 인자를 null로 주면 현재 문화권의 형식으로 해석, InvariantCulture를 주면 문화권에 상관없이 해석
                        }
                        else
                        {
                            listBox1.Items.Add("Error");
                        }
                    }
                }
            }
            catch (FileNotFoundException)
            {
                MessageBox.Show($"{filePath} 파일이 존재하지 않습니다.");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"오류 발생: {ex.Message}");
            }
        }

        private void Button3_Click(object? sender, EventArgs e)
        {
            Form2 dlg = new Form2();
            dlg.Owner = this; // Form1을 Form2의 소유자로 설정
            // 아니면 아래 if문에서 dlg.ShowDialog(this) 명령으로 Form1을 Form2의 소유자로 설정할 수도 있다
            if (dlg.ShowDialog() == DialogResult.OK)
                ; // Form2에서 OK 버튼을 클릭하면 처리할 내용 작성

            dlg.Close();
            dlg.Dispose();
        }
    }
}

 

Visual Studio - Project - Add Form(Windows Forms)... 을 클릭하고 Form을 하나 추가한다.

 

버튼을 하나 배치한다.

 

NuGet Package Manager - LiveChartsCore.SkiaSharpView.WinForms 를 설치한다.

 

using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.WinForms;

using System.Collections.ObjectModel;
using System.Diagnostics;

namespace DateTimeValue
{
    public partial class Form2 : Form
    {
        private readonly ObservableCollection<DateTimePoint> values = new ObservableCollection<DateTimePoint>();

        public Form2()
        {
            InitializeComponent();

            button1.DialogResult = DialogResult.OK; // 버튼 클릭 시 DialogResult를 OK로 설정
            this.Load += Form2_Load; // Form2 로드 이벤트에 핸들러 추가                        
        }

        private async void Form2_Load(object? sender, EventArgs e)
        {
            Form1? parent = Owner as Form1; // Form1 인스턴스를 가져옴

            if (parent != null)
                if (string.IsNullOrEmpty(parent.filePath))
                {
                    // 파일 경로가 설정되지 않은 경우, 메시지 박스를 표시하고 종료
                    MessageBox.Show("파일 경로가 설정되지 않았습니다.");
                    // Form2 종료
                    Close();
                }
                else
                {
                    try
                    {
                        FileStream fileStream = new FileStream(
                            parent.filePath,
                            FileMode.Open, // 파일이 없으면 예외 발생
                            FileAccess.Read, // 읽기 전용
                            FileShare.ReadWrite); // 다른 프로세스가 파일을 읽을 수 있도록 허용, 내가 읽는 동안 다른 프로세스도 읽기/쓰기 가능

                        using (StreamReader reader = new StreamReader(fileStream, System.Text.Encoding.UTF8))
                        {
                            string? line;
                            while ((line = await reader.ReadLineAsync()) != null)
                            {
                                //listBox1.Items.Add(line);

                                // 공백 문자를 기준으로 분리
                                string[] parts = line.Split(' ');
                                if (parts.Length == 2)
                                {
                                    values.Add(new DateTimePoint(DateTime.ParseExact(parts[0], "yyyyMMdd-HHmmss", null), double.Parse(parts[1])));
                                    // ParseExact() 세번째 인자를 null로 주면 현재 문화권의 형식으로 해석, InvariantCulture를 주면 문화권에 상관없이 해석
                                }
                                else
                                {
                                    Debug.WriteLine($"잘못된 형식의 데이터: {line}");
                                }
                            }

                            ISeries[] series = new ISeries[]
                            {
                                new LineSeries<DateTimePoint>
                                {
                                    Values = values,
                                    Fill = null
                                }
                            };

                            CartesianChart cartesianChart = new CartesianChart
                            {
                                Series = series,
                                ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X,
                                Location = new System.Drawing.Point(0, 0),
                                Size = new System.Drawing.Size(800, 300),
                                Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom,
                                // X축 설정
                                XAxes = new Axis[]
                                {
                                    //new Axis
                                    //{
                                    //    Labeler = value => new DateTime((long)value).ToString("HH:mm:ss"),
                                    //    MinStep = TimeSpan.FromSeconds(1).Ticks // 1초 단위 (Ticks 단위)
                                    //    // MinStep을 설정하면 축의 최소 간격을 지정할 수 있는데 DateTimePoint 데이터를 사용하는
                                    //    // 경우에는 제대로 동작하지 않는다. 이 경우에는 DateTimeAxis를 사용하는 것이 더 적합하다.
                                    //}

                                    // 1초 단위 축 생성 및 DateTime 포맷터 지정
                                    // 위 주석 처리된 Axis를 직접 생성하는 것보다 DateTimeAxis를 사용하는 것이 더 간단하고 직관적이다
                                    new DateTimeAxis(TimeSpan.FromSeconds(1), date => date.ToString("HH:mm:ss"))
                                    //{
                                    //    Name = "Time",
                                    //    MinStep = TimeSpan.FromSeconds(2).Ticks, // 최소 2초 간격 유지를 원하는 경우
                                    //    LabelsRotation = 15 // 라벨 회전 각도
                                    //}
                                },
                                // Y축 설정
                                //YAxes = new Axis[]
                                //{
                                //    new Axis
                                //    {
                                //        Labeler = value => value.ToString("F2") // 소수점 2자리까지 표시
                                //    }
                                //}
                            };

                            Controls.Add(cartesianChart);
                        }
                    }
                    catch (FileNotFoundException)
                    {
                        MessageBox.Show($"{parent.filePath} 파일이 존재하지 않습니다.");
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show($"오류 발생: {ex.Message}");
                    }
                }
        }
    }
}

 

프로젝트를 빌드하고 실행한다.

 

Record 버튼을 클릭해 로그 파일을 생성하고 Show 버튼을 클릭해 원하는 로그 파일을 선택하면 리스트 박스에 로그가 표시된다.

 

Form1에서 Graph 버튼을 클릭하면 Form2가 열리고 그래프가 표시된다.

 

마우스로 Zooming 및 Panning을 할 수 있다.

※ 참고

2026.04.20 - [C#] - [LiveCharts] 설치 및 기본 차트 그리기

 

반응형
Posted by J-sean
:
반응형

우선 DateTime 클래스와 TimeSpan 클래스를 사용해 보자.

 

using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            DateTime now = DateTime.Now;
            Console.WriteLine(now);

            TimeSpan hours = TimeSpan.FromHours(3);
            Console.WriteLine(now - hours);

            TimeSpan timeSpan = new TimeSpan(3, 30, 30); // 3 hours, 30 minutes, 30 seconds
            Console.WriteLine(now - timeSpan);

            DateTime past1 = new DateTime(2026, 1, 1);
            Console.WriteLine(now - past1); // TimeSpan

            string str = "20260101";
            DateTime past2 = DateTime.ParseExact(str, "yyyyMMdd", null);
            Console.WriteLine(now - past2); // TimeSpan

            string str2 = "20260101-153030";
            DateTime past3 = DateTime.ParseExact(str2, "yyyyMMdd-HHmmss", null);
            Console.WriteLine(now - past3); // TimeSpan            

            TimeSpan diff = now - past3;
            Console.WriteLine(new TimeSpan(diff.Days, diff.Hours, diff.Minutes, diff.Seconds));
            Console.WriteLine($"{diff.Days}일 {diff.Hours}시간 {diff.Minutes}분 {diff.Seconds}초");
        }
    }
}

 

 

이번엔 DateTimePicker로 날짜와 시간을 선택하고 DateTime 클래스를 사용해 보자.

 

Form에 DateTimePicker, TextBox, Button을 적당히 배치한다.

 

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

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        string date;
        string time;
        string dateTime;
        string[] mkvFiles;
        DateTime[] mkvFilesInDateTime;

        public Form1()
        {
            InitializeComponent();

            // 현재 날짜와 시간 초기화
            date = DateTime.Now.ToString("yyyyMMdd");
            time = DateTime.Now.ToString("HHmmss");
            dateTime = date + "-" + time;

            // 이벤트 핸들러 등록
            dateTimePicker1.ValueChanged += dateTimePicker1_ValueChanged;
            dateTimePicker2.ValueChanged += dateTimePicker2_ValueChanged;

            // DateTimePicker1 설정, 이렇게 하면 날짜와 시간 모두 선택할 수 있다.
            //dateTimePicker1.Format = DateTimePickerFormat.Custom;
            //dateTimePicker1.CustomFormat = "yyyy-MM-dd HH:mm:ss";

            // DateTimePicker2 설정, 시간과 분만 선택할 수 있도록 설정한다.
            dateTimePicker2.Format = DateTimePickerFormat.Custom;
            dateTimePicker2.CustomFormat = "HH:mm"; // 시간과 분만 표시하여 초 선택을 없앰
            dateTimePicker2.ShowUpDown = true; // 시간 선택을 위한 UpDown 컨트롤 표시

            // TextBox 설정
            textBox1.Multiline = true;
            textBox1.Height = textBox1.Font.Height * 6;

            // Button 이벤트 핸들러 등록
            button1.Click += button1_Click;

            // MKV 파일 검색 및 TextBox에 출력
            GetMkvFiles();
        }

        private void dateTimePicker1_ValueChanged(object sender, EventArgs e)
        {
            DateTime selectedDate = dateTimePicker1.Value;

            date = selectedDate.ToString("yyyyMMdd");
            dateTime = date + "-" + time;
        }

        private void dateTimePicker2_ValueChanged(object sender, EventArgs e)
        {
            DateTime selectedTime = dateTimePicker2.Value;
            // 초 선택이 없으므로 "HHmm00"으로 초를 항상 00으로 고정
            time = selectedTime.ToString("HHmm00");
            dateTime = date + "-" + time;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 버튼 클릭 시 dateTime이 비어있지 않으면 작업 수행
            if (!string.IsNullOrEmpty(dateTime))
            {
                DateTime selectedDateTime = DateTime.ParseExact(dateTime, "yyyyMMdd-HHmmss", null);
                DateTime closestDateTime = DateTime.MinValue;
                string closestFile = string.Empty;

                // mkvFilesInDateTime 배열의 각 요소를 순회하면서 selectedDateTime과 비교하여 이전 시점의 가장 가까운 파일을 찾는다.
                foreach (DateTime fileDateTime in mkvFilesInDateTime)
                {
                    if (fileDateTime <= selectedDateTime)
                    {
                        if (fileDateTime > closestDateTime)
                        {
                            closestDateTime = fileDateTime;
                            closestFile = fileDateTime.ToString("yyyyMMdd-HHmmss") + ".mkv";
                        }
                    }
                }

                if (!string.IsNullOrEmpty(closestFile))
                    MessageBox.Show("가장 가까운 파일: " + closestFile);
                else
                    MessageBox.Show("선택한 날짜와 시간보다 이전에 생성된 파일이 없습니다.");
            }
            else
            {
                MessageBox.Show("날짜와 시간을 선택해주세요.");
            }
        }

        private void GetMkvFiles()
        {
            // 검색할 대상 폴더 경로 지정
            string directoryPath = @".\Video";
            // 문자열 앞에 @를 붙이면 이스케이프 시퀀스를 무시하고 문자열을 그대로 사용할 수 있다.
            // 1. 이스케이프 시퀀스(\) 무시 (파일 경로에 유용)
            //  보통 C# 문자열 안에서 백슬래시(\)는 이스케이프 문자로 쓰이기 때문에(\n, \t 등), 파일 경로를 적을 때 백슬래시를
            //  두 번씩 적어줘야 한다. 하지만 @를 붙이면 백슬래시를 있는 그대로 인식한다.
            // 2. 여러 줄(Multi-line) 문자열 작성
            //  @를 사용하면 \n이나 Environment.NewLine을 쓰지 않고도 엔터를 쳐서 여러 줄의 문자열을 그대로 작성할 수 있다.

            //  경로가 존재하는지 확인
            if (Directory.Exists(directoryPath))
            {
                // 지정된 폴더에서 .mkv 확장자를 가진 모든 파일의 전체 경로 배열을 가져온다.
                mkvFiles = Directory.GetFiles(directoryPath, "*.mkv");

                // 파일 목록을 TextBox에 출력
                textBox1.Clear();
                mkvFilesInDateTime = new DateTime[mkvFiles.Length];

                for (int i = 0; i < mkvFiles.Length; i++)
                {
                    // Path.GetFileName(file)를 사용하면 파일 이름(확장자 포함)을 가져올 수 있다.
                    mkvFiles[i] = Path.GetFileNameWithoutExtension(mkvFiles[i]);

                    // 파일 이름에서 날짜와 시간 부분을 추출하여 mkvFilesInDateTime 배열에 저장
                    mkvFilesInDateTime[i] = DateTime.ParseExact(mkvFiles[i], "yyyyMMdd-HHmmss", null);

                    textBox1.AppendText(mkvFiles[i] + Environment.NewLine);
                }
            }
            else
            {
                MessageBox.Show("지정된 폴더를 찾을 수 없습니다.");
            }
        }
    }
}

 

위 코드를 빌드하고 실행한다.

 

Video 폴더에는 위와 같은 파일이 있다.

 

적당한 날짜(26/03/27)와 시간(09:36)을 선택하고 버튼을 클릭한다.

 

26/03/27 09:36보다 작으면서 가장 큰 시간의 파일은 20260327-091500.mkv이다.

 

반응형
Posted by J-sean
: