"파일로 저장" 버튼을 만들 때 저장할 위치와 이름을 사용자가 고르게 하려면 SaveFileDialog를 씁니다. 아래 코드는 버튼을 누르면 저장 대화상자를 띄우고, 사용자가 고른 파일에 클립보드의 텍스트를 써 넣습니다.
<Obsolete>
Private Sub button13_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button13.Click
Dim myStream As Stream
Dim saveFileDialog1 As SaveFileDialog = New SaveFileDialog()
saveFileDialog1.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
saveFileDialog1.FilterIndex = 2
saveFileDialog1.RestoreDirectory = True
If saveFileDialog1.ShowDialog() = DialogResult.OK Then
If (IT_HUB_TemClass.checked(myStream, saveFileDialog1.OpenFile())) IsNot Nothing Then
Dim byteData() As Byte
byteData = Encoding.ASCII.GetBytes(Clipboard.GetText)
'myStream = New FileStream("streamtest.txt", FileMode.Append)
myStream.Write(byteData, 0, byteData.Length)
myStream.Close()
End If
End If
End Sub
Private Class IT_HUB_TemClass
<Obsolete("Please refactor calling code to use normal Visual Basic assignment")>
Shared Function checked(Of T)(ByRef target As T, value As T) As T
target = value
Return value
End Function
End Class
대화상자 설정
Filter는 "표시 이름|패턴" 쌍을 |로 이어 붙인 문자열입니다. 여기서는 "txt files"와 "All files" 두 항목이 생기고, FilterIndex = 2라서 처음 열릴 때 두 번째 항목인 모든 파일이 선택되어 있습니다. 번호는 0이 아니라 1부터 셉니다. RestoreDirectory = True는 대화상자에서 폴더를 옮겨 다녀도 닫힌 뒤에 프로그램의 현재 폴더를 원래대로 돌려놓으라는 설정입니다. 상대 경로로 다른 파일을 읽는 프로그램이라면 이걸 켜 두는 게 안전합니다.
ShowDialog()가 DialogResult.OK일 때만 저장하고, 취소를 누르면 아무것도 하지 않습니다. 사용자가 이미 있는 파일을 고르면 덮어쓸지 묻는 확인 창은 기본으로 뜹니다(OverwritePrompt 기본값 True).
checked 도우미는 왜 있나
C#에서는 if ((myStream = saveFileDialog1.OpenFile()) != null)처럼 조건문 안에서 대입과 비교를 한 번에 할 수 있습니다. VB.NET은 =가 조건식 안에서는 비교로 해석되기 때문에 이렇게 쓸 수 없죠. C# 코드를 자동 변환기로 옮기면 이 부분을 흉내 내려고 "값을 대입하고 그 값을 돌려주는" 도우미 함수를 만들어 넣는데, 그게 checked입니다. <Obsolete> 특성도 변환기가 "나중에 보통 VB 대입문으로 고치라"고 표시해 둔 것이라 기능과는 관계없습니다.
그대로 쓰면 한글이 깨집니다
Encoding.ASCII.GetBytes는 ASCII 범위 밖의 문자를 전부 ?로 바꿉니다. 클립보드에 한글이 있으면 저장된 파일에는 물음표만 남습니다. VB.NET답게 정리하면서 인코딩을 UTF-8로 바꾼 코드입니다.
Private Sub button13_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button13.Click
Using dlg As New SaveFileDialog()
dlg.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
dlg.FilterIndex = 2
dlg.RestoreDirectory = True
If dlg.ShowDialog() = DialogResult.OK Then
Dim byteData() As Byte = Encoding.UTF8.GetBytes(Clipboard.GetText)
Using myStream As Stream = dlg.OpenFile()
myStream.Write(byteData, 0, byteData.Length)
End Using
End If
End Using
End Sub
Using을 쓰면 쓰는 도중에 예외가 나도 스트림이 닫혀서 파일이 잠긴 채로 남지 않습니다. 대화상자도 Dispose가 필요한 개체라 같이 Using으로 감쌌습니다. 바이트를 직접 다룰 필요가 없다면 IO.File.WriteAllText(dlg.FileName, Clipboard.GetText, Encoding.UTF8) 한 줄로 끝낼 수도 있습니다. 파일 위쪽에 Imports System.IO, Imports System.Text가 있어야 Stream, Encoding을 그대로 쓸 수 있습니다.
기본 파일 이름과 폴더 미리 채워 두기
대화상자가 뜰 때 파일 이름 칸이 비어 있으면 사용자가 매번 이름을 쳐야 합니다. 날짜가 들어간 기본 이름과 처음 열릴 폴더를 미리 정해 두면 대부분 확인만 누르고 끝납니다.
dlg.FileName = "memo_" & Date.Now.ToString("yyyyMMdd") & ".txt"
dlg.DefaultExt = "txt"
dlg.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
DefaultExt 는 사용자가 확장자 없이 이름만 입력했을 때 붙여 줄 확장자입니다. FilterIndex = 2 로 "모든 파일"이 선택된 상태에서는 확장자 없는 파일이 만들어질 수 있어서, 텍스트 파일만 저장하는 기능이라면 FilterIndex 를 1로 두는 게 낫습니다.
OpenFile은 기존 내용을 지운다
SaveFileDialog.OpenFile 은 파일을 새로 만드는 모드로 엽니다. 이미 있는 파일을 골랐다면 덮어쓰기 확인 후 내용이 전부 지워지고 처음부터 써집니다. 원래 코드에 주석으로 남아 있는 FileMode.Append 줄은 기존 내용 뒤에 이어 붙이려던 흔적인데, 그렇게 하려면 OpenFile 대신 New FileStream(dlg.FileName, FileMode.Append) 으로 직접 열어야 합니다.
클립보드에 텍스트가 없을 때도 생각해 둘 만합니다. 이미지를 복사해 둔 상태라면 Clipboard.GetText 가 빈 문자열을 돌려줘서 0바이트 파일이 만들어집니다. 저장 전에 Clipboard.ContainsText() 로 확인하고 없으면 안내 메시지를 띄우는 편이 사용자에게 친절합니다.
인코딩도 한 가지 차이가 있습니다. Encoding.UTF8.GetBytes 로 직접 쓰면 BOM이 붙지 않고, File.WriteAllText(..., Encoding.UTF8) 은 BOM을 붙여 저장합니다. 최근 메모장은 둘 다 잘 열지만, 한글 파일을 오래된 프로그램이나 엑셀에서 열어야 한다면 BOM이 있는 쪽이 덜 깨집니다.