jueves, 21 de febrero de 2013

ISO/IEC 25010:2011 - Characteristics summary

The  international  standard  ISO/IEC 25010 (ISO/IEC 9126) defines two quality models that provides a consistent terminology for specifying, measuring and evaluating system and software product quality:
  • quality  in  use  model  composed  of  five  characteristics that relate to the outcome of interaction when a product is used in a particular context of use.
  • product  quality  model  composed  of  eight  characteristics that relate to static properties of software and dynamic properties of the computer system.
Quality in use model

Quality in use characteristics and subcharacteristics
  • Effectiveness: accuracy and completeness with which users achieve specified goals.
  • Efficiency: resources expended in relation to the accuracy and completeness with which users achieve goals.
  • Satisfaction: degree to which user needs are satisfied when a system is used in a specified context of use.
    • Usefulness: user is satisfied with their perceived achievement of pragmatic goals.
    • Trust: stakeholders has confidence that a product or system will behave as intended.
    • Pleasure: user obtains pleasure from fulfilling their personal needs.
    • Comfort: user is satisfied with physical comfort.
  • Freedom from risk: degree to which a system mitigates the risk to economic status, human life, health, or the environment.
    • Economic risk mitigation: system mitigates the potential risk to financial status in the intended contexts of use. 
    • Health and safety risk mitigation: system mitigates the potential risk to people in the intended contexts of use.
    • Environmental risk mitigation: system mitigates the potential risk to property or the environment in the intended contexts of use.
  • Context coverage: degree to which a system can be used with effectiveness, efficiency, freedom from risk and satisfaction in specified contexts of use and in contexts beyond those initially explicitly identified.
    • Context completeness: system can be used in all the specified contexts of use.
    • Flexibility: system can be used in contexts beyond those initially specified in the requirements

Product quality model

Product quality characteristics and subcharacteristics

  • Functional suitability: degree to which a system provides functions that meet stated needs when used under specified conditions.
    • Functional completeness: degree to which the set of functions covers all the specified tasks and user objectives.
    • Functional correctness: system provides the correct results with the needed degree of precision.
    • Functional appropriateness: the functions facilitate the accomplishment of specified tasks and objectives.
  • Performance efficiency: performance relative to the amount of resources used under stated conditions.
    • Time behaviour: response, processing times and throughput rates of a system, when performing its functions, meet requirements.
    • Resource utilization: the amounts and types of resources used by a system, when performing its functions, meet requirements.
    • Capacity: the maximum limits of a product or system parameter meet requirements.
  • Compatibility: degree to which a system can exchange information with systems, and/or perform its required functions, while sharing the same hardware or software environment.
    • Co-existence: product can perform its functions efficiently while sharing environment and resources with other products.
    • Interoperability: a system can exchange information with other systems and use the information that has been exchanged.
  • Usability: degree to which a system can be used with effectiveness, efficiency and satisfaction in a specified context of use.
    • Appropriateness recognizability: users can recognize whether a system is appropriate for their needs, even before it is implemented.
    • Learnability: system can be used to achieve specified goals of learning to use the system.
    • Operability: system has attributes that make it easy to operate and control.
    • User error protection: system protects users against making errors.
    • User interface aesthetics: user interface enables pleasing and satisfying interaction for the user.
    • Accessibility: system can be used by people with the widest range of characteristics and capabilities.
  • Reliability: degree to which a system performs specified functions under specified conditions for a specified period of time.
    • Maturity: system meets needs for reliability under normal operation.
    • Availability: system is operational and accessible when required for use.
    • Fault tolerance: system operates as intended despite the presence of hardware or software faults.
    • Recoverability: system can recover data affected and re-establish the desired state of the system is case of an interruption or a failure.
  • Security: degree to which a system protects information and data so that persons or other products or systems have the degree of data access appropriate to their types and levels of authorization.
    • Confidentiality: system ensures that data are accessible only to those authorized to have access.
    • Integrity: system prevents unauthorized access to, or modification of, computer programs or data.
    • Non-repudiation: actions or events can be proven to have taken place, so that the events or actions cannot be repudiated later.
    • Accountability: actions of an entity can be traced uniquely to the entity.
    • Authenticity: the identity of a subject or resource can be proved to be the one claimed.
  • Maintainability: degree of effectiveness and efficiency with which a system can be modified by the intended maintainers.
    • Modularity: system is composed of components such that a change to one component has minimal impact on other components.
    • Reusability: an asset can be used in more than one system, or in building other assets.
    • Analysability: effectiveness and efficiency with which it is possible to assess the impact of an intended change.
    • Modifiability: system can be effectively and efficiently modified without introducing defects or degrading existing product quality.
    • Testability: effectiveness and efficiency with which test criteria can be established for a system.
  • Portability: degree of effectiveness and efficiency with which a system can be transferred from one hardware, software or other operational or usage environment to another.
    • Adaptability: system can effectively and efficiently be adapted for different or evolving hardware, software or usage environments.
    • Installability: effectiveness and efficiency with which a system can be successfully installed and/or uninstalled.
    • Replaceability: product can be replaced by another specified software product for the same purpose in the same environment.

lunes, 6 de agosto de 2012

Localización (traducción) con Entity Framework

Aquí comparto una forma para localizar (o traducir) entidades de una base de datos SQL mediante Entity Framework.


Me he topado con el problema de la traducción a diferentes idiomas de los datos de base de datos SQL para aplicaciones que utilizan .NET Entity Framework.

Aquí, se propone una solución simple que consiste en unos pocos cambios a una base de datos existente (nuevas tablas de traducción, sin modificación de las tablas existentes), y un método para traducir automáticamente entidades de datos mapeadas con Entity Framework.




jueves, 24 de marzo de 2011

Juego de la vida en C#

Aquí comparto un simulador de juego de la vida hecho en C# 4.0 (WinForms).

Características:

- Variables como Algoritmo, Edad máxima, Ocupación y Tamaño del universo son configurables.
(Presionar F1 para ver la ayuda de las teclas rápidas para modificar estas variables).

- Uso de PLinq para aprovechar al máximo la capacidad de procesadores multinúcleo.

- Uso de Bloqueo de bits (LockBits) para agilizar el rendimiento de la manipulación de la imagen del universo.

Capturas de algunos de los patrones que se pueden lograr:






Presionar F1 para mostrar la ayuda en pantalla.

Algunos algoritmos interesantes que puedes probar (presionando la tecla A) son:

AlgoritmoDescripcion
5678/35678(caótico) diamantes, catástrofes
/3(estable) casi todo es una chispa
1358/357(caótico) un reino equilibrado de amebas
23/3(caótico) "Juego de la Vida de Conway"
23/36(caótico) "HighLife" (tiene replicante)
235678/3678(estable) mancha de tinta que se seca rápidamente
34/34(crece) "Vida 34"


Descarga el código fuente desde Aquí
Si sólo quieres el ejecutable: Aquí

Notas: Necesitarás el .NET Framework 4.0 para poder correr el simulador.

Aquí: http://gols.codeplex.com la última versión en inglés



viernes, 18 de marzo de 2011

Herramientas de comparación y merge en TFS

Los que usamos TFS (Team Foundation Server) para el control de código fuente, sabemos que la herramienta de merge por defecto (Diffmerge) es bastante pobre.

Por suerte Visual Studio 2010 permite configurar el software para comparaciones y merge de código que hagamos dentro de TFS.

Aquí les ofrezco una breve comparación entre Diffmerge y otras dos populares herramientas (WinMerge y KDiff), además de los pasos necesarios para configurarlas dentro de Visual Studio.


Diffmerge (TFS)

• Es la herramienta por defecto del TFS para comparaciones y merges.
• No hay posibilidad de cambiar el tamaño de la fuente.
• La visibilidad del cambio es por línea, no se indica la parte de la línea que cambió.
• Interfaz poco intuitiva y no configurable.


KDiff3 (sourceforge)

• Posibilidad de cambiar el tamaño de la fuente para mayor visibilidad.
• Los cambios se resaltan con códigos de colores en la parte de la línea que sufrió cambios.
• La parte inferior muestra claramente los conflictos que deben ser resueltos manualmente.
• Interfaz configurable.
• Es open source (GNU GENERAL PUBLIC LICENSE)


• Posibilidad de cambiar el tamaño de la fuente para mayor visibilidad.
• Los cambios se resaltan con colores en la parte de la línea que sufrió cambios.
• La parte inferior muestra claramente la diferencia en el conflicto seleccionado.
• Permite visualizar rápidamente todos los cambios del archivo.
• Es open source (GNU GENERAL PUBLIC LICENSE)
• Interfaz configurable.

Estas dos últimas herramientas (WinMerge y KDiff) son bastante similares y oferecen prácticamente la misma funcionalidad, pero sin duda son las mejores herramientas de merge para Windows.
Cuestión de gusto o costumbre cuál prefiera cada uno.

Para configurar el KDiff (o cualquier otra herramienta de merge) dentro del Visual Studio, se procede de la siguiente manera:

1. Dentro de VS2010, menú Tools, Options. Navegar hasta la opción Source Control, Visual Studio TFS. Click en el botón Configure User Tools.

2. En Configure User Tools, agregar un nuevo comando con el botón Add, para asociar el comando de
Compare.

3. Ingresar los siguientes valores del comando de compare y darle OK:
Extension: *
Operation: Compare
Command: Ruta del ejecutable kdiff3
Arguments: %1 --fname %6 %2 --fname %7

4. Agregar otra nueva operación con el botón Add, esta vez para asociar el comando Merge, e ingresar los siguientes valores.
Extension: *
Operation: Merge
Command: Ruta del ejecutable kdiff3
Arguments: %3 --fname %8 %2 --fname %7 %1 --fname %6 -o %4


Listo, con esta configuración las comparaciones y merges que el TFS necesite hacer, se harán a través de la herramienta KDiff3.


Para configurar el WinMerge se procede de la misma forma, lo único que cambia es el comando y los argumentos de los últimos dos pasos, quedando así:

Extension: *
Operation: Compare
Command: winmerge.exe
Arguments: /ub /dl %6 /dr %7 %1 %2
Extension: *
Operation: Merge
Command: winmerge.exe
Arguments: /ub /dl %6 /dr %7 %1 %2 %4

Aquí transcribo una lista de herramientas con los argumentos que deben utilizarse para integrarlas con Visual Studio:
Compare
HerramientaCommandArguments
TFS defaultdiffmerge.exe%1 %2 %6 %7 %5 /ignorespace
WinDiffwindiff.exe%1 %2
DiffDoc (for Word files)DiffDoc.exe/M%1 /S%2
WinMergewinmerge.exe/ub /dl %6 /dr %7 %1 %2
Beyond Comparebc2.exe%1 %2 /title1=%6 /title2=%7
KDiff3kdiff3.exe%1 --fname %6 %2 --fname %7
Araxiscompare.exe/wait /2 /title1:%6 /title2:%7 %1 %2
Compare It!Wincmp3.exe%1 /=%6 %2 /=%7
SourceGear DiffMergeDiffMerge.exe/title1=%6 /title2=%7 %1 %2
Beyond Compare 3BComp.exe%1 %2 /title1=%6 /title2=%7
TortoiseMergeTortoiseMerge.exe/base:%1 /mine:%2 /basename:%6 /minename:%7
Visual SlickEditwin\vsdiff.exe%1 %2

Merge
HerramientaCommandArguments
TFS defaultdiffmerge.exe/merge %1 %2 %3 %4 %6 %7
KDiff3kdiff3.exe%3 --fname %8 %2 --fname %7 %1 --fname %6 -o %4
Visual SourceSafessexp.exe/merge %1 %2 %3 %4 %6 %7
Araxiscompare.exe/wait /swap /a3 /3 /title1:%6 /title2:%7 /title3:%8 %1 %2 %3 %4
Beyond Compare (2-way merge)bc2.exe%1 %2 /savetarget=%4 /title1=%6 /title2=%7
WinMerge (2-way merge)winmerge.exe/ub /dl %6 /dr %7 %1 %2 %4
Guiffyguiffy.exe-s -h1%6 -h2%7 -hm%9 %1 %2 %3 %4
Ellie Computingguimerge.exe--mode=merge3 %3 %1 %2 --to=%4 --title0=%8 --title1=%6 --title2=%7 --to-title=%9
SourceGear DiffMergeDiffMerge.exe/title1=%6 /title2=%8 /title3=%7 /result=%4 %1 %3 %2
Beyond Compare 3BComp.exe%1 %2 %3 %4 /title1=%6 /title2=%7 /title3=%8 /title4=%9
TortoiseMergeTortoiseMerge.exe/base:%3 /mine:%2 /theirs:%1 /basename:%8 /minename:%7 /theirsname:%6 /merged:%4 /mergedname:%9
Visual SlickEditwin\vsmerge.exe%3 %1 %2 %4



martes, 1 de febrero de 2011

Visual Studio Addin para operaciones con TFS

Microsoft TFS (Team Foundation Server) es un producto de Microsoft para el control de código fuente, elementos de trabajo y administración de proyectos.
A pesar de que es una herramienta bastante completa y que está totalmente integrada al IDE de Visual Studio, en mi opinión, esta integración carece de accesos rápidos a operaciones básicas que a veces hacen perder un poco el tiempo.

Por ejemplo, si queremos ubicar a un archivo dentro del control de código fuente, debemos navegar "manualmente" el árbol de archivos del servidor en la ventana Source Control Explorer.


Sería muy práctico poder acceder a un archivo del servidor directamente desde el Solution Explorer.
Ésta es una de las dos cosas que permite este Addin. Agrega menúes contextuales para poder acceder rápidamente a un archivo dentro del Source Control Explorer así como a la historia del mismo.





Actualmente existe un Addin mucho más completo (TFSPlus) que permite estas dos cosas (y algunas más), pero hasta el momento no existe una versión que funcione en Visual Studio 2010.

Para crear un Addin en Visual Studio 2010, podemos crear un nuevo proyecto de tipo Visual Studio Addin y se generará automáticamente la clase Connect, necesaria para la implementación de cualquier Addin.
Aquí hay un tutorial al respecto que está muy bien explicado, así que me limitaré sólo al código para manejar los objetos de TFS.
Para poder acceder a los objetos de TFS, necesitamos algunas referencias a librerías que están en los namespaces Microsoft.VisualStudio.TeamFoundation, y Microsoft.TeamFoundation. Normalmente se instalan en la GAC al instalar Visual Studio 2010. Pero también se pueden encontrar en C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0.

Aquí hay un ejemplo (un poco viejo pero vigente) de cómo crear un addin para comunicarse con TFS en VS2005.
La clase principal para acceder a la funcionalidad de Version Control de TFS se llama VersionControlExt. Para obtener una referencia a esta clase, podemos utilizar el objeto application que se recibe en el método OnConnect de la clase Connect de nuestro addin:

public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
 DTE2 applicationObject = (DTE2)application;
 VersionControlExt vc = applicationObject.GetObject("Microsoft.VisualStudio.TeamFoundation.VersionControl.VersionControlExt") as VersionControlExt;
...

Para ubicar y mostrar un archivo en el Source Control Explorer de TFS, se puede proceder de la siguiente forma:

string localPath = applicationObject.SelectedItems.Item(1).ProjectItem.FileNames[0];
string serverPath = vc.Explorer.Workspace.GetServerItemForLocalItem(localPath);
vc.Explorer.Navigate(serverPath);
applicationObject.ExecuteCommand("View.TfsSourceControlExplorer");
En la primera línea se obtiene la ruta local del primer archivo seleccionado en el Solution Explorer.

Luego se llama al método GetServerItemForLocalItem que obtene la ruta del archivo en el servidor a partir de su ruta local.
La tercera línea provoca que se seleccione el archivo en el Source Control Explorer de TFS.
La cuarta línea es sólo para asegurar que la ventana del explorador esté abierta y enfocada.

Adicionalmente se muestra el código necesario para mostrar la historia de un archivo (aunque esta opción ya existe en VS2010 en el menú contextual view history):

VersionControlHistoryExt history = vc.History;
string localPath = applicationObject.SelectedItems.Item(1).ProjectItem.FileNames[0];
history.Show(localPath, VersionSpec.Latest, 0, RecursionType.Full);

En la primera línea, se obtiene una referencia a la clase VersionControlHistoryExt que nos permite manipular el History de los archivos en el repositorio.
La tercerá línea provoca que se muestre la historia del archivo seleccionado.


La versión completa del Addin se puede descargar desde Aquí.

 

domingo, 9 de enero de 2011

Clipboard To Speech. Sintetizar el texto del portapapeles en C#

Clipboard 2 Speech es una pequeña aplicación en C# que observa el contenido del clipboard (o portapapeles) y cuando detecta cualquier texto en él, lo "sintetiza vocalmente" (Text-To-Speech).

Utiliza la clase SpeechSynthesizer del namespace System.Speech.Synthesis de .NET.
No es mucha la documentación que se puede encontrar de esta clase en MSDN, pero su uso es extremadamente sencillo; con sólo dos líneas de código, el sintetizador estará hablando:

SpeechSynthesizer ss = new SpeechSynthesizer();
ss.Speak("Hello, this is a test");
Para el monitoreo del contenido del clipboard, se creó la clase ClipboardWatcher (basada en un ejemplo de Tom Archer) que utiliza la función SetClipboardViewer de la API de windows, con el fin de suscribirnos al sistema operativo para que nos envíe un mensaje cada vez que el contenido de clipboard se modifique.

Esta es la forma correcta de enlazarnos a la cadena del clipboard:
private IntPtr nextClipboardViewer;
const int WM_DRAWCLIPBOARD = 0x308;
const int WM_CHANGECBCHAIN = 0x030D;
[DllImport("User32.dll")]
protected static extern int SetClipboardViewer(int hWndNewViewer);
[DllImport("User32.dll")]
public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
public ClipboardWatcher() // Constructor
{
    nextClipboardViewer = (IntPtr)SetClipboardViewer((int)this.Handle);
}
protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_DRAWCLIPBOARD:
            OnClipboardData();
            SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
            break;
        case WM_CHANGECBCHAIN:
            if (m.WParam == nextClipboardViewer)
                nextClipboardViewer = m.LParam;
            else
                SendMessage(nextClipboardViewer, m.Msg, m.WParam, m.LParam);
            break;
        default:
            base.WndProc(ref m);
            break;
    }
}
protected override void Dispose(bool disposing)
{
    ChangeClipboardChain(this.Handle, nextClipboardViewer);
}
Este código debe estar dentro de un control de windows forms que implemente la función WndProc, (por ejemplo un Form o un UserControl).

Hasta aquí sólo obtenemos un aviso cada vez que el contenido del clipboard se modifica, por eso en el método OnClipboardData debemos obtener el nuevo contenido mediante una llamada al método estático GetDataObject de la clase Clipboard (de System.Windows.Form), y a partir de esto se dispara un evento con algunos datos del contenido del clipboard:

private void OnClipboardData()
{
    ClipboardDataArgs args = new ClipboardDataArgs();
    try
    {
        args.Data = Clipboard.GetDataObject();
        if (args.Data.GetDataPresent(DataFormats.Text))
        {
            args.Text = (string)args.Data.GetData(DataFormats.Text);
        }
        else if (args.Data.GetDataPresent(DataFormats.Bitmap))
        {
            args.Image = (Bitmap)args.Data.GetData(DataFormats.Bitmap);
        }
        OnRaiseClipboardData(args);
    }
    catch (Exception e)
    {
        MessageBox.Show(e.ToString());
    }
}
public class ClipboardDataArgs : EventArgs
{
    public string Text { get; set; }
    public Bitmap Image { get; set; }
    public IDataObject Data { get; set; }
}

Simple...

Puedes descargar el código completo desde aquí.

Nota:
La aplicación acepta ciertos "comandos" desde el clipboard. Por ejemplo, puedes copiar la cadena "/pause" o "/stop" para que el sintetizador se detenga, o la cadena "/start" para que reanude la síntesis.

viernes, 24 de diciembre de 2010

Monitorear las ventanas que se abren y cierran en windows mediante un system hook

Aquí se presenta una forma de monitorear las ventanas de Windows a medida que se crean y destruyen, en C#.

Si no te interesa seguir leyendo, aquí puedes bajar el código completo de la aplicación.

Introducción

Dentro de Windows, toda interacción entre ventanas y con el usuario (clicks, movimiento de cursor, etc) se hace a través de mensajes.

Las aplicaciones tienen la capacidad de escuchar y procesar cada uno de los mensajes, por ejemplo, para minimizarse, maximizarse o cerrarse cuando el usuario hace click en el botón correspondiente de la parte superior derecha de la ventana.

En general, una aplicación Windows Forms en .NET, no tiene la necesidad de procesar explícitamente los mensajes, ya que la mayoría de las interacciones se produce automáticamente o por medio de eventos comunes de cada control (Click, Load, etc).

Pero para procesar los mensajes explícitamente, se utiliza una técnica denominada hooking que consiste en hacer que nuestra aplicación implemente un método y lo informe al sistema operativo. Este método será en realidad una función de callback (callback function) y funcionará como un "gancho" (hook) para interceptar los mensajes y poder procesarlos.

Así, cuando Windows procese un evento "enganchable", como el movimiento del cursor o la creación de una ventana, llamará inmediatamente a la función de callback que nuestra aplicación defina.
En el caso de Windows Forms, el método (la función de callback) que intercepta los mensajes, se denomina WndProc y tiene la siguiente firma:
void WndProc(ref System.Windows.Forms.Message m)

Ejemplo

Se presenta una aplicación simple que mostrará en tiempo real una lista con las ventanas, a medida que se van creando o destruyendo.
Para esto, se implementará un hook a nivel shell, para que cada vez que se cree o destruya una ventana, el sistema operativo pase el control a una subrutina de nuestra aplicación y así poder informar al usuario del evento.

A continuación se muestra el código necesario para registrar nuestra aplicación como gancho (o hook) de los mensajes del shell de windows en un Window Form en C#.
uMsgNotify = Win32API.RegisterWindowMessage("SHELLHOOK");
Win32API.RegisterShellHookWindow(this.Handle);  

La llamada a RegisterWindowMessage se hace para obtener un identificador único de los mensajes. Este identificador lo necesitaremos dentro de la función de callback para identificar a los mensajes que nos interesa escuchar.
La llamada a RegisterShellHookWindow se hace para registrar nuestro WinForm como gancho de los eventos del shell de windows.

Una vez hecho esto, procedemos a sobreescribir la función de callback en nuestro WinForm, para interceptar los mensajes de WindowCreated y WindowDestroyed, como se muestra a continuación:
protected override void WndProc(ref System.Windows.Forms.Message m)
{
    IntPtr hWnd; 
    if (m.Msg == uMsgNotify)
    {
        switch (m.WParam.ToInt32())
        {
            case (int)Win32API.ShellEvents.HSHELL_WINDOWCREATED:
                hWnd = m.LParam;
                // La ventana cuyo handler es hWnd, fue creada
                ...
                break;
            case (int)Win32API.ShellEvents.HSHELL_WINDOWDESTROYED:
                hWnd = m.LParam;
                // La ventana cuyo handler es hWnd, fue cerrada
                ...
                break;
        }
    }
    base.WndProc(ref m);
}

Lo único que resta por hacer, es obtener el nombre de la ventana que fue creada/destruida, así como el nombre del proceso (ejecutable) al que pertenece, todo a partir del identificador de la ventana (hWnd).

Para obtener el nombre de la ventana a partir de su identificador, utilizamos la función GetWindowTextLength y GetWindowText como se muestra a continuación:
private string GetWindowName(IntPtr hwnd)
{
    StringBuilder sb = new StringBuilder();
    int longi = Win32API.GetWindowTextLength(hwnd) + 1;
    sb.Capacity = longi;
    Win32API.GetWindowText(hwnd, sb, sb.Capacity);
    return sb.ToString();
}

Obtener el nombre del ejecutable al que pertenece la ventana es un poco más tedioso, ya que se necesitan 4 llamadas a la API:

private string ExePathFromHwnd(IntPtr hWnd)
{
    StringBuilder sb = new StringBuilder(Win32API.MAX_PATH);
    int pid = 0;
    Win32API.GetWindowThreadProcessId(hWnd, ref pid);
    IntPtr hProc = Win32API.OpenProcess(Win32API.ProcessAccess.AllAccess, false, pid);
    if (hProc != IntPtr.Zero)
    {
        uint lRet = Win32API.GetModuleFileNameEx(hProc, IntPtr.Zero, sb, Win32API.MAX_PATH);
        Win32API.CloseHandle(hProc);
    }
    return sb.ToString();
}

Juntando todo esto en una aplicación, podemos generar algo así:


Aquí puedes bajar el código completo de la aplicación.

Feliz navidad !