using System;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
// 퍼포먼스 카운터의 카테고리를 가져와서 각 카테고리의 이름, 유형, 도움말을 출력하고,
// 각 카테고리에 속한 인스턴스와 카운터를 출력하는 코드
// 리스트가 너무 길어질 수 있으므로, "Processor Information" 카테고리에 속한 카운터만 출력하도록 필터링
PerformanceCounterCategory[] categories = PerformanceCounterCategory.GetCategories();
foreach (PerformanceCounterCategory category in categories)
{
Console.WriteLine("Category name: {0}", category.CategoryName);
Console.WriteLine("Category type: {0}", category.CategoryType);
Console.WriteLine("Category help: {0}", category.CategoryHelp);
string[] instances = category.GetInstanceNames();
if (instances.Any())
{
foreach (string instance in instances)
{
if (category.InstanceExists(instance))
{
PerformanceCounter[] countersOfCategory = category.GetCounters(instance);
foreach (PerformanceCounter pc in countersOfCategory)
{
if (pc.CategoryName == "Processor Information")
{
Console.WriteLine("■ Category: {0}, ■ Counter: {1}, ■ Instance: {2}", pc.CategoryName, pc.CounterName, instance);
}
}
}
}
}
else
{
PerformanceCounter[] countersOfCategory = category.GetCounters();
foreach (PerformanceCounter pc in countersOfCategory)
{
if (pc.CategoryName == "Processor Information")
{
Console.WriteLine("Category: {0}, counter: {1}", pc.CategoryName, pc.CounterName);
}
}
}
}
}
}
}
위 결과를 이용해 CPU 사용량을 측정해 보자.
using System;
using System.Diagnostics;
using System.Threading;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
string categoryName = "Processor Information";
string counterName = "% Processor Utility";
string instanceName = "_Total";
float cpuPercent = 0.0f;
PerformanceCounter cpuCounter = new PerformanceCounter(categoryName, counterName, instanceName);
// categoryName: The category of the performance counter, in this case "Processor Information".
// counterName: The specific counter to monitor, in this case "% Processor Utility".
// instanceName: The instance of the counter, in this case "_Total" for overall CPU usage.
// The first call to NextValue() returns 0, so we call it once before entering the loop
cpuCounter.NextValue();
while (true)
{
Thread.Sleep(1000);
cpuPercent = cpuCounter.NextValue();
Console.WriteLine("CPU Usage: {0}%", cpuPercent);
}
}
}
}
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.WinForms;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using LiveChartsCore.Defaults;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
private bool _isChartAdded = false;
private System.Windows.Forms.Timer? panTimer = null;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
button2.Click += Button2_Click;
}
private void Button2_Click(object? sender, EventArgs e)
{
// 타이머는 Button1_Click에서 차트를 생성할 때 초기화 된다.
if (panTimer == null)
return;
if (panTimer.Enabled)
{
panTimer.Stop();
button2.Text = "Play";
}
else
{
panTimer.Start();
button2.Text = "Pause";
}
}
private void Button1_Click(object? sender, EventArgs e)
{
if (_isChartAdded) return;
_isChartAdded = true;
DateTime now = DateTime.Now;
// 비디오 존재 여부를 나타내는 타임라인 데이터 생성 (1: 비디오 있음, 0: 비디오 없음)
System.Collections.Generic.List<DateTimePoint> valuesList = new System.Collections.Generic.List<DateTimePoint>();
DateTime currentTime = now.AddDays(-2); // 2일 전부터 시작
Random r = new Random();
for (int i = 0; i < 50; i++) // 50회의 녹화/끊김 반복 (총 100개의 데이터셋)
{
// 비디오 시작
valuesList.Add(new DateTimePoint(currentTime, 1));
// 비디오 녹화 지속 시간 (10분 ~ 120분)
currentTime = currentTime.AddMinutes(r.Next(10, 121)); // AddMinutes는 DateTime 객체에 지정된 분을 더하는 메서드입니다. r.Next(10, 121)은 10부터 120까지의 랜덤한 정수를 생성하여 녹화 지속 시간을 결정합니다.
valuesList.Add(new DateTimePoint(currentTime, 0));
// 다음 녹화까지 비어있는(녹화 안 됨) 시간 (5분 ~ 60분)
currentTime = currentTime.AddMinutes(r.Next(5, 61));
}
// 진짜 데이터의 첫 시간과 마지막 시간 보관 (초기 화면 영역 설정 시 활용)
long realMinTicks = valuesList[0].DateTime.Ticks;
long realMaxTicks = valuesList[valuesList.Count - 1].DateTime.Ticks;
DateTimePoint[] values = valuesList.ToArray();
// 화면을 드래그할 때 마지막 데이터가 중앙에 올 수 있으려면,
// 화면 절반 이상의 "보이지 않는 여백 데이터"가 필요합니다.
// 앞뒤로 전체 데이터 길이/2만큼 X축의 패닝 한계를 늘려줍니다.
TimeSpan paddingSpan = TimeSpan.FromTicks(realMaxTicks - realMinTicks);
DateTime dummyStart = new DateTime(realMinTicks).Subtract(paddingSpan / 2);
DateTime dummyEnd = new DateTime(realMaxTicks).Add(paddingSpan / 2);
ISeries[] series = new ISeries[]
{
// StepLineSeries: 실제 데이터를 그립니다.
new StepLineSeries<DateTimePoint>
{
Values = values,
GeometrySize = 0, // 데이터 포인트 마커 숨김
GeometryFill = null, // 마우스 오버 시 나타나는 투명/반투명 마커 내부 숨김
GeometryStroke = null, // 마우스 오버 시 나타나는 투명/반투명 마커 외곽선 숨김
MiniatureShapeSize = 0, // 툴팁 안의 작은 마커를 0으로 만들어 숨김
Fill = new SolidColorPaint(SKColors.LightBlue.WithAlpha(120)), // 블록 내부 색상
Stroke = new SolidColorPaint(SKColors.DodgerBlue) { StrokeThickness = 1 }, // 블록 외곽선
// YToolTipLabelFormatter를 빈 문자열로 설정하여 0, 1 값이 툴팁에 표시되지 않도록 합니다.
YToolTipLabelFormatter = chartPoint => string.Empty,
// XToolTipLabelFormatter를 통해 원하는 텍스트만 표시합니다.
XToolTipLabelFormatter = chartPoint => string.Empty
// 툴팁에 날짜와 비디오 존재 여부를 표시하려면 아래와 같이 XToolTipLabelFormatter를 설정할 수 있습니다.
// 이 내용이 표시되게 하려면 아래 CartesianChart 생성 부분에서 TooltipPosition을 Hidden이 아닌 다른 값으로 설정해야 합니다. (예: TooltipPosition.Top)
//XToolTipLabelFormatter = chartPoint =>
//{
// if (chartPoint?.Model == null) return string.Empty;
// // SkiaSharp 기본 툴팁 렌더링에서 \n 같은 개행 문자를 인식하지 못하고 깨진 문자로 표시할 수 있습니다.
// // 따라서 한 줄로 풀어서 표시하거나 Environment.NewLine을 사용하는 것이 좋습니다.
// return $"{chartPoint.Model.DateTime:yyyy-MM-dd HH:mm}" + Environment.NewLine +
// (chartPoint.Model.Value == 1 ? "Video Start" : "Video End");
//}
},
// 투명한 더미 시리즈를 추가하여 X축의 "패닝 가능한 절대 영역(DataBounds)"을 강제로 늘려줍니다.
// null 값은 계산에서 무시되므로 실제 값을 넣되 투명하게 만듭니다.
new LineSeries<DateTimePoint>
{
Values = new[]
{
new DateTimePoint(dummyStart, 0),
new DateTimePoint(dummyEnd, 0)
},
Fill = null,
Stroke = null,
GeometrySize = 0,
GeometryFill = null,
GeometryStroke = null,
YToolTipLabelFormatter = chartPoint => string.Empty,
XToolTipLabelFormatter = chartPoint => string.Empty
}
};
// X축의 초기 최소/최대 기준값을 실제 데이터 범위로 설정
long minTicks = realMinTicks;
long maxTicks = realMaxTicks;
Axis xAxis = new Axis
{
UnitWidth = TimeSpan.FromMinutes(1).Ticks, // 데이터 간의 주요 단위를 1분으로 설정
MinStep = TimeSpan.FromMinutes(1).Ticks,
// MinLimit과 MaxLimit을 실제 데이터의 최소/최대 시간으로 설정하여 초기 화면에서 전체 데이터 범위를 보여줍니다.
MinLimit = realMinTicks,
MaxLimit = realMaxTicks,
};
xAxis.Labeler = value =>
{
long ticks = (long)value;
if (ticks < DateTime.MinValue.Ticks || ticks > DateTime.MaxValue.Ticks)
return string.Empty;
DateTime date = new DateTime(ticks);
// 현재 화면에 보이는 범위 (Zoom 상태)
double min = xAxis.MinLimit ?? minTicks; // ?? 의미: xAxis.MinLimit이 null이 아니면 그 값을 사용하고, null이면 minTicks를 사용한다는 의미입니다. 즉, MinLimit이 설정되어 있으면 그 값을 사용하고, 그렇지 않으면 데이터의 최소 시간으로 간주합니다.
double max = xAxis.MaxLimit ?? maxTicks;
TimeSpan visibleSpan = TimeSpan.FromTicks((long)Math.Max(0, max - min));
if (visibleSpan.TotalDays >= 1) // 1일 이상 보이면 날짜 표시
return date.ToString("MM-dd");
else if (visibleSpan.TotalHours >= 2) // 2시간 이상 보이면 시간 표시
return date.ToString("MM-dd HH:00");
else // 그 이하로 확대하면 분 단위까지 표시
return date.ToString("MM-dd HH:mm");
};
// LiveChartsCore는 다중 Y축을 지원하므로 YAxes 속성은 Axis 객체의 배열을 받습니다.
// 이 차트에서는 단일 Y축만 필요하지만 속성 타입이 배열이기 때문에 Axis 배열로 선언합니다.
Axis[] yAxes = new Axis[]
{
new Axis
{
IsVisible = false, // Y축은 비디오 존재 유무(0과 1)만 나타내므로 숨깁니다.
MinLimit = 0, // 바닥을 0으로 맞춤
MaxLimit = 1.2 // 비디오 블록(1) 상단에 여백을 조금 둠
}
};
CartesianChart cartesianChart = new CartesianChart
{
Series = series,
XAxes = new Axis[] { xAxis },
YAxes = yAxes,
TooltipPosition = LiveChartsCore.Measure.TooltipPosition.Hidden, // 툴팁 위치를 Hidden으로 설정하여 빈 툴팁 박스와 화살표를 완전히 숨깁니다.
Sections = new[]
{
// X축과 일치하는 수평선 섹션 추가 (Y=0 위치에 수평선)
new RectangularSection
{
Yi = 0, // Y축의 시작 위치
Yj = 0, // Y축의 끝 위치 (0으로 설정하여 X축과 일치)
Stroke = new SolidColorPaint { Color = SKColors.DodgerBlue, StrokeThickness = 1 }
}
},
ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X, // X: Enables zooming and panning on the X axis.
// 줌 및 팬 모드를 설정하여 X축에서만 작동하도록 지정한다.
Location = new System.Drawing.Point(0, 0),
Size = new System.Drawing.Size(800, 300),
//Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
};
// 차트 중앙에 고정된 빨간 수직선(현재 재생 위치) 추가
Panel centerLine = new Panel
{
BackColor = System.Drawing.Color.Red,
Width = 2,
Height = cartesianChart.Height,
Location = new System.Drawing.Point(cartesianChart.Left + cartesianChart.Width / 2 - 1, cartesianChart.Top),
//Anchor = AnchorStyles.Top | AnchorStyles.Bottom,
Enabled = false // 마우스 이벤트가 차트로 전달되도록 비활성화
};
// 폼의 크기가 변경될 때 차트와 중앙선을 함께 조정하여 항상 중앙에 위치하도록 합니다.
// 그런데 이 프로그램에서 차트의 사이즈는 고정되어 있기 때문에 폼의 크기가 변경되어도 차트의 크기는 변하지 않습니다.
// 그러므로 차트의 Resize 이벤트는 트리거되지 않을 것입니다. 사이즈가 변경될 때 무엇인가를 조정하려면 폼의 Resize 이벤트를 사용하는 것이 더 적절할 수 있습니다.
cartesianChart.Resize += (s, ev) =>
{
centerLine.Location = new System.Drawing.Point(cartesianChart.Left + cartesianChart.Width / 2 - 1, cartesianChart.Top); // 중앙선의 위치를 차트의 중앙으로 조정
centerLine.Height = cartesianChart.Height;
};
// 차트가 업데이트(Zoom, Pan 등) 될 때 중앙 좌표(시간)를 계산하여 텍스트박스에 표시합니다.
cartesianChart.UpdateStarted += (chart) =>
{
double min = xAxis.MinLimit ?? minTicks;
double max = xAxis.MaxLimit ?? maxTicks;
long center = (long)(min + (max - min) / 2);
if (center >= DateTime.MinValue.Ticks && center <= DateTime.MaxValue.Ticks)
{
DateTime centerTime = new DateTime(center);
// 빨간 선(중앙) 시간에 해당하는 Y값(비디오 존재 유무) 찾기
// 계단형(StepLine) 그래프이므로 현재 시간보다 작거나 같은 직전 데이터의 값을 사용합니다.
double centerValue = 0;
for (int i = values.Length - 1; i >= 0; i--)
{
if (values[i].DateTime.Ticks <= center)
{
centerValue = values[i].Value ?? 0;
break;
}
}
// 차트 업데이트 이벤트는 백그라운드 스레드에서 트리거될 수 있으므로 UI 스레드로 마샬링합니다.
if (textBox1.IsHandleCreated && !textBox1.IsDisposed)
{
textBox1.BeginInvoke(new Action(() =>
{
string status = centerValue == 1 ? "Video ON" : "Video OFF";
textBox1.Text = $"{centerTime:yyyy-MM-dd HH:mm:ss} [{status}]";
}));
}
}
};
// 1초마다 차트를 1초 분량만큼 이동시키는 타이머 설정
panTimer = new System.Windows.Forms.Timer
{
Interval = 1000 // 1000 밀리초 = 1초
};
panTimer.Tick += (s, ev) =>
{
if (xAxis.MinLimit.HasValue && xAxis.MaxLimit.HasValue)
{
long oneSecondTicks = TimeSpan.FromSeconds(1).Ticks;
// X축의 최소/최대값에 1초를 더하여 화면(카메라)을 오른쪽으로 이동시킵니다.
// 이로 인해 시각적으로 차트의 데이터가 왼쪽으로 이동(스팬)하는 효과가 납니다.
// 반대 방향 이동을 원하실 경우 += 대신 -= 를 사용하시면 됩니다.
xAxis.MinLimit += oneSecondTicks;
xAxis.MaxLimit += oneSecondTicks;
}
};
// cartesianChart 내부에 컨트롤을 추가하면 형변환 에러가 발생하므로 센터의 빨간 선은 폼의 Controls에 추가하여 차트 위에 표시되도록 합니다.
Controls.Add(centerLine);
Controls.Add(cartesianChart);
centerLine.BringToFront(); // 이 코드가 없어도 차트보다 빨간 선이 위에 표시되지만, 명시적으로 BringToFront()를 호출하여 확실히 한다.
}
}
}
Windows Forms App (.NET Framework)를 선택하면 빌드 후 실행 시 에러가 발생한다.
LiveChartsCore.SkiaSharpView.WinForms를 설치한다.
Form에 버튼을 하나 배치한다.
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
private bool _isChartDrawn = false;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object? sender, EventArgs e)
{
if (_isChartDrawn) return;
_isChartDrawn = true;
double[] values1 = new double[] { 2, 1, 3, 5, 3, 4, 6 };
int[] values2 = new int[] { 4, 2, 5, 2, 4, 5, 3 };
ISeries[] series = new ISeries[]
{
new LineSeries<double>
{
Values = values1,
Fill = null,
GeometrySize = 20
},
new LineSeries<int, StarGeometry>
{
Values = values2,
Fill = null,
GeometrySize = 20
}
};
DrawnLabelVisual title = new DrawnLabelVisual(
new LabelGeometry
{
Text = "My chart title",
Paint = new SolidColorPaint(SKColor.Parse("#303030")),
TextSize = 25,
Padding = new LiveChartsCore.Drawing.Padding(15),
// padding은 텍스트와 라벨의 경계 사이의 간격을 지정하는 속성이다.
// 위 코드에서는 15로 설정되어 있어 텍스트와 라벨의 경계 사이에 15픽셀의 간격이 생긴다.
VerticalAlign = LiveChartsCore.Drawing.Align.Start,
// vertical align은 Start, Middle, End가 있다. Start는 위쪽, Middle은 가운데, End는 아래쪽에 위치한다.
HorizontalAlign = LiveChartsCore.Drawing.Align.Start
// horizontal align은 Start, Middle, End가 있다. Start는 왼쪽, Middle은 가운데, End는 오른쪽에 위치한다.
});
CartesianChart cartesianChart = new CartesianChart
{
Series = series,
Title = title,
Location = new System.Drawing.Point(0, 0),
Size = new System.Drawing.Size(800, 300),
//Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
// 위 코드와 같이 Anchor를 지정하면 폼의 크기가 변경될 때 차트의 크기가 자동으로 조정되는데 그려진 비율이 유지되는게 아니라
// 그려지지 않은 부분의 사이즈가 그대로 유지되는 방식으로 조정된다.
};
Controls.Add(cartesianChart);
}
}
}
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.WinForms;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
private bool _isChartAdded = false;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object? sender, EventArgs e)
{
if (_isChartAdded) return;
_isChartAdded = true;
int[] values = Fetch();
ISeries[] series = new ISeries[]
{
new LineSeries<int>
{
Values = values
}
};
CartesianChart cartesianChart = new CartesianChart
{
Series = series,
ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X, // X: Enables zooming and panning on the X axis.
// 줌 및 팬 모드를 설정하여 X축에서만 작동하도록 지정한다.
Location = new System.Drawing.Point(0, 0),
Size = new System.Drawing.Size(800, 300),
//Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom
};
Controls.Add(cartesianChart);
}
private static int[] Fetch()
{
int[] values = new int[100];
Random r = new Random();
int t = 0;
for (int i = 0; i < 100; i++)
{
t += r.Next(-90, 100);
values[i] = t;
}
return values;
}
}
}
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Extensions;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
private readonly PieChart pieChart = new PieChart();
private readonly Random random = new Random();
private bool _isChartAdded = false;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object? sender, EventArgs e)
{
if (_isChartAdded) return; // 차트가 이미 추가된 경우, 중복 추가 방지
_isChartAdded = true;
double sectionsOuter = 130; // 섹션의 외부 반경 오프셋
double sectionsWidth = 20; // 섹션의 최대 반경 너비
NeedleVisual needle = new NeedleVisual
{
Value = 45 // 바늘이 가리키는 값 (pieChart MinValue와 MaxValue 사이)
};
pieChart.Series = GaugeGenerator.BuildAngularGaugeSections(
new GaugeItem(60, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.LimeGreen, s)), // 첫 번째 섹션: 60% 비율, 녹색
new GaugeItem(30, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.Gold, s)), // 두 번째 섹션: 30% 비율, 노란색
new GaugeItem(10, s => SetStyle(sectionsOuter, sectionsWidth, SKColors.Crimson, s))); // 세 번째 섹션: 10% 비율, 빨간색
pieChart.VisualElements = [
new AngularTicksVisual
{
Labeler = value => value.ToString("N1"), // 눈금 레이블 포맷, N1은 소수점 한 자리까지 표시
LabelsSize = 16,
LabelsOuterOffset = 15,
OuterOffset = 65,
TicksLength = 20
},
needle
];
pieChart.InitialRotation = -225; // 시작 각도 설정
pieChart.MaxAngle = 270; // 게이지가 차지하는 각도 설정
pieChart.MinValue = 0; // 게이지의 최소값 설정
pieChart.MaxValue = 100; // 게이지의 최대값 설정
pieChart.Location = new System.Drawing.Point(0, 0);
pieChart.Size = new System.Drawing.Size(400, 400);
//Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;
Controls.Add(pieChart);
Button b1 = new Button
{
Text = "Update",
Location = new System.Drawing.Point(400, 300),
Size = new System.Drawing.Size(100, 50)
};
b1.Click += (sender, e) => needle.Value = random.Next(0, 100);
Controls.Add(b1);
b1.BringToFront();
}
private static void SetStyle(double sectionsOuter, double sectionsWidth, SKColor color, PieSeries<ObservableValue> series)
{
series.OuterRadiusOffset = sectionsOuter; // 섹션의 외부 반경 오프셋 설정
series.MaxRadialColumnWidth = sectionsWidth; // 섹션의 최대 반경 너비 설정
series.CornerRadius = 0; // 섹션의 모서리 반경 설정
series.Fill = new SolidColorPaint(color); // 섹션의 칠하기 색상 설정
}
}
}
※ 참고
Toolbox - LiveChartsCore.SkiaSharpView.WinForms 툴은 사용하지 말자. 그리는 건 되지만 지울 수가 없다.
LiveCharts는 코드로만 작성하거나 코드로 UserControl을 만들어 Toolbox에서 사용해야 한다.
※ 참고
LiveCharts2 (화면 상단의 Framework를 WinForms 등 원하는 대로 선택해야 한다)
이번엔 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이다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Counter()
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
}
static void Main(string[] args)
{
Task task = new Task(Counter);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Action counter = () =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
};
Task task = new Task(counter);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Task task = Task.Run(() =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Task counting... {subCounter++}");
}
});
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
}
}
}
순서는 약간 달라질 수 있다.
이번엔 인수를 하나 받는 스레드를 만들어 보자. 아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Counter(object to)
{
int subCounter = 0;
for (int i = 0; i < (int)to; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
}
static void Main(string[] args)
{
Task task = new Task(Counter, 5);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Action<object> Counter = (object to) =>
{
int subCounter = 0;
for (int i = 0; i < (int)to; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
};
Task task = new Task(Counter, 5);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
}
}
}
인수가 있는 경우, Task.Run() 메서드에서 람다식으로 코드를 만드는 건 의미가 없는 것 같다.
결과는 위와 같다.
이번엔 리턴값이 있는 스레드를 만들어 보자. 아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static int Counter()
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
}
static void Main(string[] args)
{
Task<int> task = new Task<int>(Counter);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
Console.WriteLine($"Sub Thread finished with count: {task.Result}");
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Task<int> task = Task<int>.Run(() =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
});
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
Console.WriteLine($"Sub Thread finished with count: {task.Result}");
}
}
}
Action은 리턴값이 없는 메서드 전용이기 때문에 이 경우 Action을 사용할 수 없다.
순서는 약간 달라질 수 있다.
이번엔 인수도 있고 리턴값도 있는 스레드를 만들어 보자.
아래 2가지 코드는 모두 같은 내용이지만 다른 방식으로 작성되었다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static int Counter(object to)
{
int subCounter = 0;
for (int i = 0; i < (int)to; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
}
static void Main(string[] args)
{
Task<int> task = new Task<int>(Counter, 5);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
Console.WriteLine($"Sub Thread finished with count: {task.Result}");
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Func<object, int> Counter = (object to) =>
{
int subCounter = 0;
for (int i = 0; i < (int)to; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
};
Task<int> task = new Task<int>(Counter, 5);
task.Start();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
task.Wait();
Console.WriteLine($"Sub Thread finished with count: {task.Result}");
}
}
}
async, await를 사용해 보자.
아래 코드는 컴파일 에러가 발생한다. Main 함수에는 async가 단순하게 그냥 붙을 수 없다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
// 컴파일 에러 발생
static async void Main(string[] args)
{
await Task.Run(() => Count());
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
}
static void Count()
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
}
}
}
Main 함수에 단순하게 async가 붙으면 다음과 같은 에러가 발생한다.
Program does not contain a static 'Main' method suitable for an entry point
또, 아래와 같이 바꾸면 단일 스레드처럼 동작한다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static async Task Main(string[] args)
{
await Task.Run(() => Count());
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
}
static void Count()
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
}
}
}
await 키워드는 이 비동기 작업(서브 스레드)이 완전히 끝날 때까지 여기서 기다리겠다는 의미이기 때문이다.
아래와 같이 바꿔보자.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static async Task Main(string[] args)
{
// await 없이 Task를 실행(시작)만 하고 변수에 담아둔다.
// 이 시점부터 서브 스레드가 백그라운드에서 동작한다.
Task countTask = Task.Run(() => Count());
// 메인 스레드는 멈추지 않고 자신의 루프 작업을 동시에 수행한다.
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
// 참고: 비동기 메서드(async) 내에서는 Thread.Sleep 대신 await Task.Delay를 사용하는 것을 권장.
await Task.Delay(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
// 메인 스레드의 작업이 다 끝나면, 메인 프로세스가 종료되기 전에
// 서브 스레드의 작업이 모두 완료되었는지 마지막으로 확인(대기).
await countTask;
}
static void Count()
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
}
}
}
아니면 아래와 같이 다른 함수에서 async, await를 사용하도록 코드를 바꾼다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Count();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
}
static async void Count()
{
await Task.Run(async () =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
// Use Task.Delay instead of Thread.Sleep to avoid blocking the thread
await Task.Delay(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
});
}
}
}
이번엔 반환값이 있는 함수를 살펴보자.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Task<int> taskResult = Count();
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
int result = taskResult.Result;
Console.WriteLine($"Result from Sub Thread: {result}");
}
static async Task<int> Count()
{
return await Task.Run(() =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
});
}
}
}
위 코드를 아래와 같이 바꾸면 단일 스레드처럼 작동하게 된다. 조심하자.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int taskResult = Count().Result;
// Task가 시작되자마자 바로 Result를 요구하고 있다.
// 메인 스레드가 진행하지 못하게 된다.
Console.WriteLine($"Result from Sub Thread: {taskResult}");
int mainCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Main Thread counting... {mainCounter++}");
}
}
static async Task<int> Count()
{
return await Task.Run(() =>
{
int subCounter = 0;
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"Sub Thread counting... {subCounter++}");
}
return subCounter;
});
}
}
}
Parallel 클래스를 사용해서 간편하게 스레드를 사용해 보자.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Parallel.For(0, 10, DoSomething);
// Alternatively, you can use a lambda expression:
//Parallel.For(0, 10, i =>
//{
// DoSomething(i);
//});
}
static void DoSomething(int i)
{
Thread.Sleep(1000);
Console.WriteLine($"Job {i} completed.");
}
}
}
하나씩 처리하면 10초가 걸리는 작업이 10개의 스레드가 생성되어 1초 만에 끝난다. (스레드는 CPU 코어의 갯수에 따라 다르게 생성된다)
using System;
using System.Diagnostics;
using System.IO;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
// ProcessStartInfo 설정
ProcessStartInfo startInfo = new ProcessStartInfo();
//startInfo.FileName = "cmd.exe"; // 실행할 프로그램 (예: cmd)
//startInfo.Arguments = "/c dir"; // 프로그램 인자 (예: 현재 디렉토리 파일 목록)
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/c dir";
startInfo.UseShellExecute = false; // 쉘 사용 안 함 (리다이렉션 필수 설정)
startInfo.RedirectStandardOutput = true; // 표준 출력 리다이렉션
startInfo.CreateNoWindow = true; // 콘솔 창 띄우지 않음
// 프로세스 실행
using (Process process = Process.Start(startInfo))
{
// 3. 출력 내용 읽기
using (StreamReader reader = process.StandardOutput)
{
// 전체 결과 가져오기. 너무 길면 메모리 문제 발생 가능
//string result = reader.ReadToEnd();
//Console.WriteLine("--- 외부 프로그램 결과 ---");
//Console.WriteLine(result);
//Console.WriteLine("--------------------------");
// 한 줄씩 읽기
string line;
Console.WriteLine("--- 외부 프로그램 결과 ---");
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
//Console.Write(line + Environment.NewLine);
}
Console.WriteLine("--------------------------");
}
process.WaitForExit(); // 프로그램 종료 대기
}
}
}
}
namespace ConsoleApp1
{
using System.Globalization;
internal class Program
{
static void Main(string[] args)
{
var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
foreach (var ci in allCultures)
{
// Display the name of each culture.
Console.Write($"{ci.EnglishName} ({ci.Name}): ");
// Indicate the culture type.
if (ci.CultureTypes.HasFlag(CultureTypes.NeutralCultures))
Console.Write(" NeutralCulture");
if (ci.CultureTypes.HasFlag(CultureTypes.SpecificCultures))
Console.Write(" SpecificCulture");
Console.WriteLine();
}
Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);
double value = 1234.56;
Console.WriteLine(value);
Console.WriteLine(value.ToString());
// The current culture is a property of the executing thread.
Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
// The invariant culture is culture-insensitive; it's associated with the English
// language but not with any country/region.
Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);
Console.WriteLine(value);
Console.WriteLine(value.ToString());
Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));
CultureInfo.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine("Current culture is {0}", CultureInfo.CurrentCulture.Name);
Console.WriteLine(value);
Console.WriteLine(value.ToString());
Console.WriteLine(value.ToString(CultureInfo.CurrentCulture));
Console.WriteLine(value.ToString(CultureInfo.InvariantCulture));
}
}
}
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Random random = new Random(); // 난수 생성기
// byte 배열 난수 생성
byte[] bytes = new byte[10];
random.NextBytes(bytes);
foreach (byte b in bytes)
{
Console.Write($"{b}, ");
}
Console.WriteLine();
// 난수 섞기
random.Shuffle<byte>(bytes);
foreach (byte b in bytes)
{
Console.Write($"{b}, ");
}
Console.WriteLine();
// 문자열 리스트 섞기
List<string> fruits = new List<string> { "사과", "바나나", "딸기", "오렌지", "수박", "멜론", "키위" };
string[] fruitsArray = fruits.ToArray();
random.Shuffle<string>(fruitsArray);
foreach (string fruit in fruitsArray)
{
Console.Write(fruit + ", ");
}
Console.WriteLine();
// 정수 리스트 생성
List<int> numbers = Enumerable.Range(1, 10).ToList();
foreach (int i in numbers)
{
Console.Write(i + ", ");
}
Console.WriteLine();
// 정수 배열 섞기
int[] numbersArray = numbers.ToArray();
random.Shuffle<int>(numbersArray);
foreach (int i in numbersArray)
{
Console.Write(i + ", ");
}
Console.WriteLine();
// 섞인 정수 배열 일부 선택
foreach (int i in numbersArray[2..^5]) // '^'는 뒤에서 부터 인덱싱. ^5는 파이썬의 -5와 동일.
{
Console.Write(i + ", ");
}
Console.WriteLine();
// 리스트 컴프리헨션 기능
List<int> squared = (from x in numbers
where x % 2 == 0 // 짝수라는 조건이 필요 없다면 삭제 가능.
select x * x).ToList();
foreach (int i in squared)
{
Console.Write(i + ", ");
}
Console.WriteLine();
}
}
}
정답(2)을 포함하는 선택지를 만들어 보자.
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Random random = new Random(); // 난수 생성기
// 1~10 정수 리스트 생성
List<int> numbers = Enumerable.Range(1, 10).ToList();
// 특정 숫자(2)를 포함하는 선택지 4개 만들기
List<int> choices = new List<int>();
choices.Add(2); // 2는 무조건 포함
while (choices.Count < 4)
{
int i = random.Next(0, numbers.Count);
if (!choices.Contains(numbers[i]))
choices.Add(numbers[i]);
}
// 선택지 섞기
choices = choices.Shuffle<int>().ToList();
foreach (int i in choices)
{
Console.Write(i + ", ");
}
Console.WriteLine();
}
}
}
난수 생성기 없이 만들 수도 있다.
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.Shuffle();
// 1~10 정수를 생성하고 섞기
IEnumerable<int> choices = [2]; // 선택지에 2는 무조건 포함
foreach (int n in numbers)
{
if (!choices.Contains(n))
choices = choices.Append(n);
if (choices.Count() >= 4)
break;
}
choices = choices.Shuffle();
foreach (int i in choices)
Console.Write(i + ", ");
}
}
}