A las configuraciones existentes que guardaban las medidas de la ventana
cuando ésta se cerraba (width, height, left, top) se añaden otras 2:
una para guardar el modo de inicio de la aplicación (en ventana o a pantalla
completa) y otra para guardar el nombre de la cultura por defecto (español):
Se accede a ellas mediante la clase MySettings usando la palabra clave My, como vemos en este código referido a Ventana:
'obtener los datos de la configuración guardada al cerrar el formulario por última vez ' Select Case My.Settings.Ventana 'si se configura para arrancar en ventana Case "full_no" Me.Width = My.Settings.Anchura Me.Height = My.Settings.Altura Me.Left = My.Settings.Izquierda Me.Top = My.Settings.Derecha 'si se configura para arrancar a pantalla completa Case "full" Me.WindowState = FormWindowState.Maximized End Select
Y también se puede modificar su valor y guardarlo al salir de la aplicación; el método específico que guarda la configuración es ChangeAndPersistSettings() pero también se puede utilizar en su lugar la opción "Save My.Settings on shutdown" de la pestaña Application en las propiedades del proyecto; lo vemos de nuevo referido a Ventana:
'método específico que guarda la configuración, también se puede utilizar en su lugar la opción
'<Save My.Settings on shutdown> de la pestaña Application en las propiedades del proyecto
Sub ChangeAndPersistSettings()
'guardar las medidas de la ventana sólo si no está maximizada o minimizada
'y si My.Settings.Ventana está configurado como "full_no"
If (Me.WindowState = FormWindowState.Normal) AndAlso (My.Settings.Ventana = "full_no") Then
My.Settings.Altura = Me.Height
My.Settings.Anchura = Me.Width
My.Settings.Izquierda = Me.Left
My.Settings.Derecha = Me.Top
End If
'
'My.Settings.Cultura = lengua
'guardar la configuración
My.Settings.Save()
End Sub
Siempre resulta más cómodo que los programas tengan un formulario centralizado en el que poder modificar sus parámetros. En este ejercicio se consigue mediante un formulario de tipo modal (diálogo) que permite al usuario seleccionar entre varias opciones o realizar alguna acción:
Para comprender los fundamentos de la localización de aplicaciones .NET recomiendo
leer los referidos artículos del Guille pues mejor que en ellos no lo podría
explicar, aquí sólo voy a mostrar el resultado conseguido y a remarcar a los
no iniciados que el proceso es bastante sencillo y al alcance de cualquiera.
Al cambiar la elección del idioma, ha de modificarse en todos los formularios
de la aplicación, incluido por supuesto el de opciones. Como ejemplo, éste es
el formulario de opciones cuando la interfaz está en español:

Y éste es el mismo formulario con la interfaz en inglés:

El cambio instantáneo del idioma de la interfaz se puede conseguir fácilmente mediante la propiedad CurrentCulture del hilo actual, como en este código:
Select Case cbIdiomas.SelectedIndex
'el ítem 1 (índice 0) es "Español (España)"
Case 0
lengua = "es-ES"
Case 1
'el ítem 2 (índice 1) es "Inglés (Estados Unidos)"
lengua = "en-US"
End Select
My.Settings.Cultura = lengua
'
Thread.CurrentThread.CurrentCulture = New CultureInfo(lengua)
Thread.CurrentThread.CurrentUICulture = New CultureInfo(lengua)
Pero he optado por cerrar el hilo actual del programa y arrancar un hilo nuevo con la cultura (idioma) recién seleccionada. Para ello, desde el formulario de opciones llamo a un método del formulario principal:
'si el ítem seleccionado del comboBox no se corresponde con el valor
'de la cultura del hilo actual, llamar al método nuevoHilo()
If Not (Thread.CurrentThread.CurrentCulture.Name = lengua) Then
Call nuevoHilo()
End If
El método nuevoHilo crea un nuevo hilo (thread) para enlazar con el método nuevoForm que arranca una instancia diferente del formulario, después de que se cierre la instancia actual. Antes de poder arrancar el nuevo proceso con un control ActiveX de la clase WebBrowser es necesario configurar su "ApartmentState", por ello antes de arrancar el segundo proceso se configura su "ApartmentState" al modo STA (un apartamento -Apartment- es un contenedor lógico dentro de un proceso para los objetos que tienen las mismas características de comunicación con los subprocesos):
Public Shared Sub nuevoHilo()
'cerrar el formulario y que se vuelva a abrir automáticamente para que
'en el nuevo inicio lea correctamente la configuración de idioma y cultura
'
'crear un nuevo hilo (thread) para enlazar con el método nuevoForm() que arranca
'otra instancia diferente de Form1 después de que se cierre la instancia actual
Dim hilo As New Thread(AddressOf nuevoForm)
hilo.SetApartmentState(ApartmentState.STA)
hilo.Start()
'cerrar la aplicación y su hilo actual
Application.Exit()
End Sub
'método que inicia de nuevo la aplicación
Shared Sub nuevoForm()
'detenerse 0,4 segundos
Thread.Sleep(400)
'iniciar de nuevo la aplicación en el nuevo hilo
Application.Run(New Form1)
End Sub
NOTA (ayuda MSDN): "Un apartamento es un contenedor
lógico dentro de un proceso para los objetos que comparten requisitos iguales
de acceso a los subprocesos. Todos los objetos que se encuentran en el mismo
apartamento pueden recibir llamadas de cualquier subproceso del apartamento.
NET Framework no utiliza apartamentos, y los objetos administrados son responsables
de utilizar los recursos compartidos de forma segura para la ejecución de subprocesos.
Dado que las clases COM utilizan apartamentos, el CRL necesita crear e inicializar
un apartamento cuando llama a un objeto COM en una situación de interoperabilidad
COM. Un subproceso administrado puede crear y entrar en un apartamento de un
único subproceso (STA: que sólo acepta un subproceso), o un apartamento
multiproceso (MTA: que contiene uno o varios subprocesos); hay un tercer
ApartmentState indeterminado, cuando el estado no se ha establecido todavía.
Puede controlar el tipo de apartamento creado estableciendo la propiedad ApartmentState
del subproceso en uno de los valores de la enumeración ApartmentState. Puesto
que un subproceso determinado sólo puede inicializar un apartamento COM una
única vez, no se puede cambiar el tipo de apartamento después de la primera
llamada al código no administrado."
No hay que olvidar traducir los cuadros de diálogo o cualquier otro elemento cuyo aspecto dependa de la cultura activa:
'mensaje después de la operación, diferente según la cultura
Select Case lengua.Substring(0, 2).ToLower()
Case "es"
MsgBox("Operación completada correctamente.", MsgBoxStyle.Information, "Info")
Case Else
MsgBox("Operation succesfully completed.", MsgBoxStyle.Information, "Info")
End Select
Para cambiar el idioma de la interfaz sin reiniciar la aplicación es necesario cambiar la propiedad CurrentCulture del hilo actual y volver a recrear el formulario y todos sus controles (en caso de no hacerlo así, nos encontraríamos con controles duplicados, uno por cada idioma, conservaríamos los del idioma anterior y aparecerían los del idioma recién seleccionado):
'ADOPTAR EL IDIOMA SELECCIONADO EN EL FORMULARIO Dialog1.vb Y
'RECREAR EL FORMULARIO Y SUS CONTROLES PARA QUE SE MUESTREN EN EL IDIOMA CORRECTO
Shared Sub cambiarIdioma()
'
My.Settings.urlIdioma = Form1.wbHtml.Url.ToString
'guardar las medidas de la ventana sólo si no está maximizada o minimizada
'y si My.Settings.Ventana está configurado como "full_no"
If (Form1.WindowState = FormWindowState.Normal) Then
My.Settings.Altura = Form1.Height
My.Settings.Anchura = Form1.Width
My.Settings.Izquierda = Form1.Left
My.Settings.Arriba = Form1.Top
End If
'aplicar el idioma seleccionado en cbIdiomas al hilo actual del programa
Thread.CurrentThread.CurrentCulture = New CultureInfo(lengua)
Thread.CurrentThread.CurrentUICulture = New CultureInfo(lengua)
'
'recrear el formulario y sus controles para que se muestren en el idioma correcto
Form1.Controls.Clear()
Form1.InitializeComponent()
'conservar el tamaño de la ventana al cambiar de idioma
Select Case My.Settings.Ventana
'si se configura para arrancar en ventana
Case "full_no"
Form1.Width = My.Settings.Anchura
Form1.Height = My.Settings.Altura
Form1.Left = My.Settings.Izquierda
Form1.Top = My.Settings.Arriba
'si se configura para arrancar a pantalla completa
Case "full"
Form1.WindowState = FormWindowState.Maximized
End Select
Form1.Navegar(My.Settings.urlIdioma)
End Sub
La lista de aplicaciones del sistema se guarda en la clave
HKEY_CLASSES_ROOT\Applications
y la lista de navegadores web está en
HKEY_LOCAL_MACHINE\Software\Clients\StartMenuInternet
Podemos recuperar o almacenar datos del registro mediante la clase Microsoft.Win32.Registry pero también podemos hacerlo a partir del objeto My; en el código se observan ámbos tipos de código. La clase Registry define objetos RegistryKey que permiten manipular valores del registro con sus métodos GetValue y SetValue:
'leer y escribir en el registro de Windows
Shared Sub manejarReg()
'
'objeto RegistryKey que permite manipular valores del registro con sus métodos GetValue y SetValue
Dim clave1, clave2 As RegistryKey
'
'ruta contiene la ruta completa de la aplicación, incluido el ejecutable
'se usan comillas al principio y al final pues la ruta contiene espacios
Dim ruta As String
ruta = """" & Application.ExecutablePath & """"
'nombre contiene el nombre del ensamblado con la extensión
Dim nombre As String
nombre = System.IO.Path.GetFileName(My.Application.Info.AssemblyName)
'
'HKEY_CLASSES_ROOT\Applications
'(añadir el programa a la lista de aplicaciones)
'#1
clave1 = My.Computer.Registry.ClassesRoot.CreateSubKey("Applications\emiWeb.exe\Shell\Open\command")
clave1.SetValue("", ruta & " ""%1""", RegistryValueKind.String)
clave1 = My.Computer.Registry.ClassesRoot.CreateSubKey("Applications\emiWeb.exe\Shell\Open\ddeexec\Application")
clave1.SetValue("", "emiWeb.exe ""%1""", RegistryValueKind.String)
clave1 = My.Computer.Registry.ClassesRoot.CreateSubKey("Applications\emiWeb.exe\Shell\Open\ddeexec\Topic")
clave1.SetValue("", "WWW_OpenURL", RegistryValueKind.String)
'#2
'
'HKEY_LOCAL_MACHINE\Software\Clients\StartMenuInternet
'añadir el programa a la lista de navegadores del sistema
clave2 = My.Computer.Registry.LocalMachine.CreateSubKey("Software\Clients\StartMenuInternet\emiWeb.exe")
clave2.SetValue("", nombre, RegistryValueKind.String)
clave2 = clave2.CreateSubKey("Shell\Open\Command")
clave2.SetValue("", ruta, RegistryValueKind.String)
'
End Sub
El código relativo a clave1 (desde '#1 hasta '#2) equivale a ejecutar un archivo REG con el siguiente contenido:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\Applications\emiWeb.exe] [HKEY_CLASSES_ROOT\Applications\emiWeb.exe\Shell] [HKEY_CLASSES_ROOT\Applications\emiWeb.exe\Shell\Open] [HKEY_CLASSES_ROOT\Applications\emiWeb.exe\Shell\Open\command] @="\"D:\\PAPELES\\Visual Studio Projects\\Visual Studio 2005\\Visual Basic\\Navegador Web\\Navegador Web\\bin\\Debug\\emiWeb.exe\" \"%1\"" [HKEY_CLASSES_ROOT\Applications\emiWeb.exe\Shell\Open\ddeexec] [HKEY_CLASSES_ROOT\Applications\emiWeb.exe\Shell\Open\ddeexec\Application]@="emiWeb.exe \"%1\"" [HKEY_CLASSES_ROOT\Applications\navegadorWeb.exe\Shell\Open\ddeexec\Topic] @="WWW_OpenURL"
El resultado queda escrito en el registro como muestra la imagen: