IronPython / PythonNET - Diálogos de Selección

FolderBrowserDialog, OpenFileDialog y SaveFileDialog



Introducción


Los diálogos de selección son formularios predefinidos para seleccionar un archivo o una carpeta, de manera que el path de este archivo o carpeta puede ser utilizado por el código de la aplicación.

Dentro de System.Windows.Forms hay 3 tipos de diálogos predefinidos:

La única diferencia entre OpenFileDialog y SaveFileDialog es su título, ya que uno sugiere que va a abrirse y otro que va a guardarse un archivo. Sin embargo, lo que la aplicación haga con el path del archivo obtenido una vez se cierre el diálogo es cosa tuya, y es tu responsabilidad utilizar uno u otro cuando corresponda.

Estos diálogos son formualarios predefinidos con ciertas propiedades. Antes de empezar a utilizarlos observa el siguiente código:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')

from System.Windows.Forms import Form

form = Form()
result = form.ShowDialog()

print(repr(result)) # System.Windows.Forms.DialogResult.Cancel

Este script muestra un formulario vacío y cuando hacemos click en el botón X se cierra, momento en el que finaliza la ejecución de form.ShowDialog(), que devuelve un resultado que en ironpython siempre es DialogResult.Cancel, mientras que en pythonnet es 2 (ya que DialogResult.Cancel == 2 en pythonnet).

Los diálogos de selección siempre se usan como el formulario de este ejemplo: Tienen un método ShowDialog que devuelve un resultado.

Para el formulario vacío del ejemplo anterior, el resultado siempre es DialogResult.Cancel, pero para los diálogos de selección predefinidos el valor puede ser:

El método ShowDialog puede invocarse tantas veces como se quiera, de modo que una instancia de diálogo puede utilizarse múltiples veces:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

from System.Windows.Forms import *
from System.Drawing import *

form = Form()
label = Label()
label.TextAlign = ContentAlignment.MiddleCenter
label.Dock = DockStyle.Fill
label.Font = Font(FontFamily('Consolas'), 32.0, FontStyle.Bold, GraphicsUnit.Pixel)
form.Controls.Add(label)

for i in range(3):
    form.Text = str(i+1)
    label.Text = form.Text
    form.ShowDialog()



FolderBrowserDialog


Un FolderBrowserDialog tiene las siguientes propiedades:

Ejemplo de uso de FolderBrowserDialog:

# -*- coding: utf-8 -*-

import threading, clr

clr.AddReference('System.Windows.Forms')

from System.Windows.Forms import *



def main():

    dialog = FolderBrowserDialog()

    dialog.SelectedPath = 'C:\\' # 'C:/' no funciona, hay que usar barras invertidas
    dialog.Description = 'Haz el favor de seleccionar una carpeta...'
    dialog.ShowNewFolderButton = True

    r = dialog.ShowDialog()

    if r == DialogResult.OK:
        print('Has seleccionadao la carpeta {}'.format(dialog.SelectedPath))

    elif r == DialogResult.Cancel:
        print('No has seleccionado carpeta, última carpeta seleccionada es {}'.format(dialog.SelectedPath))



threading.Thread(target=main).start()

FolderBrowserDialog

Observa que he definido una función main que se ejecuta en un hilo separado: threading.Thread(target=main).start(). Esto es necesario para pythonnet, pero no para ironpython.

También puedes utilizar los threads de .NET para conseguir el mismo resultado:

# -*- coding: utf-8 -*-

import clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Threading')

from System.Windows.Forms import *
from System.Threading import *


def main():

    dialog = FolderBrowserDialog()

    dialog.SelectedPath = 'C:\\' # 'C:/' no funciona, hay que usar barras invertidas
    dialog.Description = 'Haz el favor de seleccionar una carpeta...'
    dialog.ShowNewFolderButton = True

    r = dialog.ShowDialog()

    if r == DialogResult.OK:
        print('Has seleccionadao la carpeta {}'.format(dialog.SelectedPath))

    elif r == DialogResult.Cancel:
        print('No has seleccionado carpeta, última carpeta seleccionada es {}'.format(dialog.SelectedPath))



thread = Thread(ThreadStart(main))
thread.SetApartmentState(ApartmentState.STA)
thread.Start()
thread.Join()



OpenFileDialog y SaveFileDialog


Tanto OpenFileDialog como SaveFileDialog tienen las siguientes propiedades:

SaveFileDialog además tiene la propiedad .OverwritePrompt: Su valor por defecto es True, de modo que si se selecciona un archivo que ya existe se muestra un mensaje de confirmación para que el usuario indique si quiere sobreescribir el archivo. Si su valor es False no se muestra ningún mensaje.


Ejemplo de OpenFileDialog

# -*- coding: utf-8 -*-

import threading, clr

clr.AddReference('System.Windows.Forms')

from System.Windows.Forms import OpenFileDialog, DialogResult



def main():

    dialog = OpenFileDialog()

    dialog.InitialDirectory = 'C:\\'
    dialog.DefaultExt = 'txt'
    dialog.Filter = 'txt|*.txt|all|*.*'
    dialog.FileName = 'nombre_archivo.txt'


    if dialog.ShowDialog() == DialogResult.OK:

        with open(dialog.FileName, 'r') as f:
            print(f.read())

    else:
        print('No has seleccionado ningún archivo.')



threading.Thread(target=main).start()

OpenFileDialog

Puesto que hemos utilizado un OpenFileDialog, al obtener su FileName el lógico usarlo para leer el contenido del archivo:

with open(dialog.FileName, 'r') as f:
    print(f.read())

Hemos hecho una lectura en modo texto, pero podrías utilizar open(dialog.FileName, 'rb') para hacer una lectura en modo binario.


Ejemplo de SaveFileDialog

# -*- coding: utf-8 -*-

import sys, clr

clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Threading')

from System.Windows.Forms import SaveFileDialog, DialogResult
from System.Threading import *


def main():

    user_input = raw_input if int(sys.version[0]) < 3 else input
    texto = user_input('Escribe algo: ')

    dialog = SaveFileDialog()

    dialog.InitialDirectory = 'C:\\'
    dialog.DefaultExt = 'txt'
    dialog.Filter = 'txt|*.txt|all|*.*'
    dialog.FileName = 'nombre_archivo.txt'


    if dialog.ShowDialog() == DialogResult.OK:

        with open(dialog.FileName, 'w') as f:
            f.write(texto)

    else:
        print('El texto no ha sido guardado.')



thread = Thread(ThreadStart(main))
thread.SetApartmentState(ApartmentState.STA)
thread.Start()
thread.Join()

SaveFileDialog

Puesto que hemos utilizado un SaveFileDialog, al obtener su FileName el lógico usarlo para escribir en el archivo:

    with open(dialog.FileName, 'w') as f:
        f.write(texto)

También podrías hacer una escritura binaria utilizando open(dialog.FileName, 'wb').

Una vez seleccionado el nombre de archivo no hemos comprobado si este existe o no. No es necesario hacer esta comprobación, ya que SaveFileDialog muestra un MessageBox de confirmación en caso de que el archivo ya exista, para que el usuario decida si quiere sobreescribir el archivo o no.

Si no quieres que se muestre este mensaje de confirmación puedes asignar el valor False a la propiedad SaveFileDialog.OverwritePrompt. En este caso sería lógico utilizar open(dialog.FileName, 'a'):

dialog = SaveFileDialog()

dialog.OverwritePrompt = False

if dialog.ShowDialog() == DialogResult.OK:

    with open(dialog.FileName, 'a') as f:
        f.write('texto añadido al archivo')



Ejemplo de Aplicación completa


El siguiente script es una aplicación que tiene un menú para abrir, guardar y cerrar archivos con la extensión .py. La aplicación permite editar y ejecutar scripts ironpython:

# -*- coding: utf-8 -*-

import sys, os, clr

for reference in 'System', 'System.Windows.Forms', 'System.Drawing', 'System.Threading':
    clr.AddReference(reference)

import System
from System.Windows.Forms import *
from System.Drawing import *
from System.Threading import *



class MainForm(Form):
    def __init__(self):

        self.Text = 'IronPython Editor'
        self.Icon = Icon.ExtractAssociatedIcon(sys.executable)
        self.ClientSize = Size(640, 480)
        self.Load += lambda *args: self.__setattr__('MinimumSize', self.Size) # self.MinimumSize = self.Size (una vez se ha cargado el formulario)

        # CREAR MENÚ

        self.Menu = MainMenu()

        self.open_item, self.save_item, self.close_item = [i() for i in [MenuItem] * 3]
        self.open_item.Text, self.save_item.Text, self.close_item.Text = 'Abrir', 'Guardar', 'Cerrar'

        self.open_item.Click += self.show_open_file_dialog
        self.save_item.Click += self.show_save_file_dialog
        self.close_item.Click += self.close_file

        self.save_item.Enabled = self.close_item.Enabled = False

        for i in self.open_item, self.save_item, self.close_item:
            self.Menu.MenuItems.Add(i)

        # CREAR CONTROLES

        self.rchCode = RichTextBox()
        self.rchCode.Dock = DockStyle.Fill
        self.rchCode.AcceptsTab = True
        self.rchCode.Font = Font(FontFamily('Consolas'), 14.0, FontStyle.Regular, GraphicsUnit.Pixel)
        self.rchCode.ForeColor = Color.Blue
        self.rchCode.KeyUp += self.enable_or_disable_save_item

        self.btnExec = Button()
        self.btnExec.Text = 'RUN'
        self.btnExec.Height = 50
        self.btnExec.Dock = DockStyle.Bottom
        self.btnExec.Click += self.execute_code

        self.Controls.Add(self.rchCode)
        self.Controls.Add(self.btnExec)

        # CREAR DIÁLOGOS

        self.open_file_dialog = OpenFileDialog()
        self.open_file_dialog.DefaultExt = 'py'
        self.open_file_dialog.Filter = 'scripts python|*.py;*.pyw'

        self.save_file_dialog = SaveFileDialog()
        self.save_file_dialog.DefaultExt = 'py'
        self.save_file_dialog.Filter = 'scripts python|*.py;*.pyw'

    def show_open_file_dialog(self, *args):

        if self.open_file_dialog.FileName:
            self.open_file_dialog.InitialDirectory, self.open_file_dialog.FileName = os.path.split(self.open_file_dialog.FileName)

        if self.open_file_dialog.ShowDialog() == DialogResult.OK:
            with open(self.open_file_dialog.FileName, 'rb') as f:
                self.rchCode.Text = f.read().decode('latin-1') # self.Invoke(System.Action(lambda:setattr(self.rchCode, 'Text', f.read().decode('latin-1'))))

            self.save_item.Enabled = self.close_item.Enabled = True

    def show_save_file_dialog(self, *args):

        if self.open_file_dialog.FileName:
            self.save_file_dialog.InitialDirectory, self.save_file_dialog.FileName = os.path.split(self.open_file_dialog.FileName)

        if self.save_file_dialog.ShowDialog() == DialogResult.OK:
            with open(self.save_file_dialog.FileName, 'w') as f:
                f.write(self.rchCode.Text)

            self.save_item.Enabled = self.close_item.Enabled = True

    def close_file(self, *args):
        self.rchCode.Text = ''
        self.save_item.Enabled = self.close_item.Enabled = False
        self.open_file_dialog.InitialDirectory = self.save_file_dialog.InitialDirectory = None
        self.open_file_dialog.FileName = self.save_file_dialog.FileName = None

    def enable_or_disable_save_item(self, *args):
        self.save_item.Enabled = bool(self.rchCode.Text)

    def execute_code(self, *args):
        if self.rchCode.Text:
            try:
                namespace = {} # Evitar acceso a variables de la aplicación
                exec(self.rchCode.Text, namespace)
            except Exception as e:
                MessageBox.Show(str(e), 'ERROR', MessageBoxButtons.OK, MessageBoxIcon.Error)





def main():
    Application.EnableVisualStyles()
    Application.Run(MainForm())


if sys.implementation.name == 'cpython':
    thread = Thread(ThreadStart(main))
    thread.SetApartmentState(ApartmentState.STA)
    thread.Start()
    thread.Join()

elif sys.implementation.name == 'ironpython':
    main()

Ejemplo App Completa