Etiqueta: Visual Studio

Posts relacionados con el entorno de desarrollo (IDE) de Visual Studio

  • El control RichTextBox de .NET 5.0 RC2 no permite buscar una cadena si esta está en más de una línea

     
    Nota aclaratoria del 28-oct-2020:

    Según parece lo que la documentación indica es que no se pueden buscar textos que incluyan más de una línea, no que el texto esté en más de una línea…
    Este es el texto aclaratorio que me han mandado:

    For the documentation on the FIND method, it means you cannot search in multiple lines of the Text, if you want to search different lines content, you can split the lines in different substrings. Please see the post: https://stackoverflow.com/questions/54056447/richtextbox-find-multi-line-string-in-multi-line-text

    La traducción (automática) de ese texto en inglés:
    Para la documentación sobre el método FIND, significa que no puede buscar en varias líneas del texto, si desea buscar contenido de diferentes líneas, puede dividir las líneas en diferentes subcadenas.

    A mí casi me deja igual… pero al ver el enlace y lo que el que pregunta quiere buscar, sí lo entiendo:
    Es decir, en la cadena de búsqueda no puedes usar varias líneas (con retorno de carro) de ser así devolverá -1.
    Pero si puedes buscar una cadena (que no tenga retornos de carro) y buscará en todas las líneas del texto del control RichTextBox.
    En fin…

     

    Pues eso… En la documentación lo indica claramente, pero eso será algo nuevo y se ve que también afecta a las versiones anteriores, es decir, el control RichTextBox ya no es el mismo que era antes… al menos el método Find con búsqueda de cadenas ya no funciona igual.

     

    Rectificación del 24-oct-2020 por la tarde:
    (donde dije digo digo Diego)

    Aunque la documentación dice eso, en realidad si busca aunque esté más de una vez en varias líneas y no me refiero solo a .NET Framework 4.7.2, también lo hace en .NET 5.0 RC2.
    Ayer es que estaba yo del todo grave con lo de la boca… aparte de que veo poco incluso con las gafas…
    Te explico.

    Estaba probando lo de Buscar texto en un programa que estoy haciendo y probé con buscar InitializeComponent, pero se ve que lo escribí mal: InitilizeComponent y al buscarlo no lo encontraba… ¡elemental querido Guille!
    El no comprobar si estaba o no bien escrito es porque seleccioné el texto correcto y pulsé Ctrl+F para buscar, pero se ve que en la lista de búsqueda ya estaba la palabra mal escrita y la seleccionaría sin querer…

    Y claro al probar la versión que he hecho y ver que tampoco encontraba la dichosa palabra, pues… me di cuenta que le faltaba una «a«… en fin…

     

    No lo he probado aún con el .NET Framework pero creo que el problema es el mismo.

    Nota del 24-oct-2020:
    Acabo de probarlo en un «editor» que tengo hecho con .NET Framework 4.7.2 y va bien aunque en la documentación siga diciendo que no va.

    Nota

    Los Find métodos que aceptan un string como parámetro no pueden encontrar texto contenido en más de una línea de texto dentro de RichTextBox . La realización de este tipo de búsqueda devolverá un valor de uno negativo (-1).

    A ver qué dicen esta gente si eso se va a quedar así o hay alguna alternativa… que debería haberla…

    Lo mismo estoy equivocado, pero en una aplicación en la que estoy usándolo actualmente ya no funciona como debería funcionar… y al buscar una cadena, (sabiendo que existe en el texto) me devuelve un valor -1, que es lo que «ahora» dice la documentación que ocurrirá… pero antes no ocurría… en fin…

    Esto es lo que dice la documentación:

     

    Bueno… a ver qué ocurre con esto…

    Mientras tanto voy a crear un método de extensión para añadir funcionalidad de buscar el mismo texto aunque esté en varias líneas, en principio lo llamaré FindString y será como Find(String, Int32, RichTextBoxFinds).
    Cuando lo tenga hecho te avisaré o pondré aquí el enlace 😉

     

    Nos vemos.
    Guillermo

  • Moderniza tu aplicación con el efecto grisáceo en el texto de los controles con nota de lo que hay que escribir en VB y C#

    Pues eso… seguramente habrás visto en algunas aplicaciones que en las cajas de texto (o los ComboBox) se muestra un texto en color gris para indicarte lo que puedes escribir en esa casilla.

    Para tener esa funcionalidad en nuestros proyectos no es necesario usar controles especializados ni nada de eso, ya verás que es fácil, y espero que claro, hacerlo mediante unas pocas líneas de código.

    Actualizado y corregido el código (24-oct-2020)
    Ver más abajo el comentario y el nuevo código de QuitarPredeterminado.

     

    En la figura 1 puedes ver el aspecto de esa caja de texto.

    Figura 1. La aplicación en modo ejecución

    En la imagen anterior puedes ver el texto <Escribe tu nombre> en la caja de texto, cuando empieces a escribir, ese texto informativo se quita y solo estará lo que escribas, y si borras todo el texto (dejas vacía la caja de texto) se volverá a mostrar ese texto informativo.

    ¿Dónde controlar si se debe mostrar o no el texto predeterminado?

    En mi caso, yo hago las comprobaciones en tres eventos del (en este caso) TextBox. A saber:

    El evento Enter (cuando toma el foco) ahí se comprueba si debe tener el color grisáceo o el negro del texto.
    Se comprueba si el texto es diferente del predeterminado (el de ayuda o informativo), en ese caso se asigna el color del texto a ControlText (definido en SystemColors).

    El evento Leave (cuando pierde el foco) ahí compruebo si no hay texto, en cuyo caso, asigno el texto informativo y le asigno a la propiedad ForeColor el color GrayText (también de la clase SystemColors).

    El evento TextChange (cuando el texto cambia), hay lo que hay que hacer es más elaborado (menos simple que en los casos anteriores), al menos como yo lo he hecho, que puede que haya un método más fácil y simple de hacerlo. Pero es lo que le da esa vidilla al efecto ;-).

    Yo suelo usar una variable a nivel de formulario llamada inicializando que me sirve para evitar la entrada en cascada del evento TextChange (y otros eventos, pero en este ejemplo es el que se produce cuando el texto cambia). Si el valor de esa variable es True, salimos del método.

    La primera comprobación es si el texto del control está vacío o tiene el texto predeterminado, en cuyo caso lo ponemos en color gris y le asigno el texto informativo.
    Si no se da esa comprobación, el texto ya tiene algo, por ejemplo, si se ha empezado a escribir, y compruebo si el texto que había antes era el texto vacío en cuyo caso le quito el texto predeterminado con idea de que solo se quede lo que el usuario está escribiendo.

    La primera idea que tuve fue quitar directamente el texto predeterminado con un Replace(textoPredeterminado, «»), pero eso solo vale si se está escribiendo al principio o al final del texto (que es lo más común), pero si el usuario se pone a escribir en medio del texto predeterminado, no funcionaría… por tanto, he creado una función en la que quito todo el texto predeterminado del texto que haya, para así dejar solo lo que se esté escribiendo, que será la primera letra que el usuario escriba.

    Esa función yo la tengo como una extensión de la clase String, pero en el código que te mostraré, es simplemente una función llamada QuitarPredeterminado que recibe dos argumentos, el texto a comprobar y el texto a quitar. De esa forma nos servirá para aplicarla a cualquier control y evitar tener que repetir lo mismo en todos los evento TextChange de los controles a los que queramos dar esta funcionalidad.

    Aquí (y en la comprobación anterior) entra en juego la variable inicializando, que es la que evitará que se entre nuevamente mientras modificamos el texto.

    Finalmente asignamos a la variable que contiene el texto anterior, el texto actual para que no se quite nada cuando ya no sea el texto predeterminado, ya que es posible que el usuario escriba en el texto ese mismo texto que damos por predeterminado, el problema es que el usuario solo escriba el texto predeterminado, pero… bueno, se supone que no lo escribirá… 😉

    Y esto es todo lo que hay que hacer… viendo ahora el código para Visual Basic y C# lo entenderás mejor.

     

    Rectificación del código (24-oct-2020)

    Pues resulta que hay que hacer una comprobación extra antes de quitar los caracteres del texto predeterminado, si no, puede pasar lo que me pasó anoche, que al asignar la palabra InitializeComponent a la caja de textos donde está la palabra predeterminada Buscar… pues… se encontró con la a (de Buscar) y la quitó…
    Así que… ahora compruebo primero si están todas las letras del texto predeterminado y de ser así, entonces hago el reemplazo, si no están todas las letras, simplemente se devuelve el texto original y como si nada.

    El código de QuitarPredeterminado que te muestro más abajo ya tiene las modificaciones indicadas.

    El código completo, tanto para VB como para C#, publicado en github lo actualizaré en unos minutos.

    El código de ejemplo para Visual Basic .NET y C#

    Empezaré mostrándote las variables que necesitaremos a nivel de formulario.
    Es decir, la que controla si ya estamos dentro del evento, la que contiene el texto predeterminado y la que contiene el texto anterior de la caja de texto.

    Estas dos últimas serán diferentes para cada control a los que queramos aplicar el efecto. Al menos la del texto anterior, ya que el texto predeterminado puede ser el mismo para varios controles.

    Visual Basic .NET

    Private Const textVacio As String = "<Escribe tu nombre>"
    Private textAnterior As String = textVacio
    Private inicializando As Boolean

    C#

    private const string textVacio = "<Escribe tu nombre>";
    private string textAnterior = textVacio;
    private bool inicializando;
    

     

    Ahora te muestro el código de los eventos Enter y Leave.

    Visual Basic .NET

    Private Sub txtTexto_Enter(sender As Object, e As EventArgs) Handles txtTexto.Enter
        If txtTexto.Text <> textVacio Then
            txtTexto.ForeColor = SystemColors.ControlText
        End If
    End Sub
    
    Private Sub txtTexto_Leave(sender As Object, e As EventArgs) Handles txtTexto.Leave
        If String.IsNullOrEmpty(Me.txtTexto.Text) Then '
            Me.txtTexto.ForeColor = SystemColors.GrayText
            Me.txtTexto.Text = textVacio
        End If
    End Sub
    

     

    C#

    private void txtTexto_Enter(object sender, EventArgs e)
    {
        if (txtTexto.Text != textVacio)
            txtTexto.ForeColor = SystemColors.ControlText;
    }
    
    private void txtTexto_Leave(object sender, EventArgs e)
    {
        if (string.IsNullOrEmpty(this.txtTexto.Text))
        {
            this.txtTexto.ForeColor = SystemColors.GrayText;
            this.txtTexto.Text = textVacio;
        }
    }
    

     

    El código del evento TextChanged en el que uso la función para quitar el texto predeterminado esté en la posición que esté.
    Después te muestro el código de esa función QuitarPredeterminado.

    Visual Basic .NET

    Private Sub txtTexto_TextChanged(sender As Object,
                                     e As EventArgs) Handles txtTexto.TextChanged
        If inicializando Then Return
        If txtTexto.Text = "" OrElse txtTexto.Text = textVacio Then
            txtTexto.ForeColor = SystemColors.GrayText
            inicializando = True
            txtTexto.Text = textVacio
            inicializando = False
        Else
            If textAnterior = textVacio Then
                inicializando = True
                txtTexto.Text = QuitarPredeterminado(txtTexto.Text, textVacio)
                inicializando = False
    
                txtTexto.SelectionStart = txtTexto.Text.Length
            End If
            txtTexto.ForeColor = SystemColors.ControlText
        End If
        textAnterior = txtTexto.Text
    End Sub
    
    

     

    C#

    private void txtTexto_TextChanged(object sender, EventArgs e)
    {
        if (inicializando)
            return;
        if (txtTexto.Text == "" || txtTexto.Text == textVacio)
        {
            txtTexto.ForeColor = SystemColors.GrayText;
            inicializando = true;
            txtTexto.Text = textVacio;
            inicializando = false;
        }
        else
        {
            if (textAnterior == textVacio)
            {
                inicializando = true;
                txtTexto.Text = QuitarPredeterminado(txtTexto.Text, textVacio);
                inicializando = false;
    
                txtTexto.SelectionStart = txtTexto.Text.Length;
            }
            txtTexto.ForeColor = SystemColors.ControlText;
        }
        textAnterior = txtTexto.Text;
    }
    

     

    Fíjate que antes de asignar un texto al TextBox asigno el valor true a inicializando, de esa forma, cuando se cambie el texto (al asignarlo se cambiará), no entrará nuevamente en el evento.

    Y como te comentaba antes, cuando el texto que había anterior es el predeterminado (textVacio), quitamos dicho texto de la propiedad Text de la caja de textos mediante una llamada al método QuitarPredeterminado, al que le pasamos el texto que queremos comprobar (el del control TextBox) y el que queremos quitar.

    Veamos el código de ese método… y verás lo casi retorcido que es… 🙂
    En serio, lo que hago es recorrer cada carácter del texto a quitar (el predeterminado) y quitarlo del texto, de esa forma, esté donde esté ese carácter que queremos quitar lo hará correctamente.

    Aquí tienes el código de la función QuitarPredeterminado.

    Visual Basic .NET

    ''' <summary>
    ''' Quitar de una cadena un texto indicado (que será el predeterminado cuando está vacío).
    ''' Por ejemplo si el texto grisáceo es Buscar... y
    ''' se empezó a escribir en medio del texto (o en cualquier parte)
    ''' BuscarL... se quitará Buscar... y se dejará L.
    ''' Antes de hacer cambios se comprueba si el texto predeterminado está al completo 
    ''' en el texto en el que se hará el cambio.
    ''' </summary>
    ''' <param name="texto">El texto en el que se hará la sustitución.</param>
    ''' <param name="predeterminado">El texto a quitar.</param>
    ''' <returns>Una cadena con el texto predeterminado quitado.</returns>
    ''' <remarks>18/Oct/2020 actualizado 24/Oct/2020</remarks>
    Public Function QuitarPredeterminado(texto As String, predeterminado As String) As String
        Dim cuantos = predeterminado.Length
        Dim k = 0
    
        For i = 0 To predeterminado.Length - 1
            Dim j = texto.IndexOf(predeterminado(i))
            If j = -1 Then Continue For
            k += 1
        Next
        ' si k es distinto de cuantos es que no están todos lo caracteres a quitar
        If k <> cuantos Then
            Return texto
        End If
    
        For i = 0 To predeterminado.Length - 1
            Dim j = texto.IndexOf(predeterminado(i))
            If j = -1 Then Continue For
            If j = 0 Then
                texto = texto.Substring(j + 1)
            Else
                texto = texto.Substring(0, j) & texto.Substring(j + 1)
            End If
        Next
    
        Return texto
    End Function
    

    C#

    /// <summary>
    /// Quitar de una cadena un texto indicado (que será el predeterminado cuando está vacío).
    /// Por ejemplo si el texto grisáceo es Buscar... y
    /// se empezó a escribir en medio del texto (o en cualquier parte)
    /// BuscarL... se quitará Buscar... y se dejará L.
    /// Antes de hacer cambios se comprueba si el texto predeterminado está al completo 
    /// en el texto en el que se hará el cambio.
    /// </summary>
    /// <param name="texto">El texto en el que se hará la sustitución.</param>
    /// <param name="predeterminado">El texto a quitar.</param>
    /// <returns>Una cadena con el texto predeterminado quitado.</returns>
    /// <remarks>18/Oct/2020 actualizado 24/Oct/2020</remarks>
    private string QuitarPredeterminado(string texto, string predeterminado)
    {
        var cuantos = predeterminado.Length;
        var k = 0;
    
        for (var i = 0; i < predeterminado.Length; i++)
        {
            var j = texto.IndexOf(predeterminado[i]);
            if (j == -1)
                continue;
            k += 1;
        }
        // si k es distinto de cuantos es que no están todos lo caracteres a quitar
        if (k != cuantos)
            return texto;
    
        for (var i = 0; i < predeterminado.Length; i++)
        {
            var j = texto.IndexOf(predeterminado[i]);
            if (j == -1)
                continue;
            if (j == 0)
                texto = texto.Substring(j + 1);
            else
                texto = texto.Substring(0, j) + texto.Substring(j + 1);
        }
        return texto;
    }
    

     

    Y esto es todo… yo lo estoy usando de varias formas, por ejemplo en los TextBox (en realidad un ComboBox del tipo ToolStripComboBox) para Buscar y otro para Reemplazar, en el primero muestro de forma predeterminada el texto Buscar… y el de reemplazar el texto Reemplazar….

    La idea la tomé prestada, con estas mejoras que te he mostrado aquí, del proyecto CSharp2VB de Paul1956.

     

    Publicaré Ya está el código completo de ejemplo tanto para Visual Basic como para C# (proyectos para .NET Framework 4.8, aunque sirven igualmente con proyectos para .NET 5.0 RC2) publicado en GitHub: Mostrar-texto-grisaceo.

     

    Espero que te sea de utilidad.

     

    Nos vemos.
    Guillermo

  • Detectar varias pulsaciones de teclas en aplicación de Windows Forms (código para C# y Visual Basic)

    Pues eso… necesitaba saber cómo detectar varias pulsaciones de teclas al estilo de Ctrl+K, Ctrl+C y similares, es decir, se pulsa la tecla Control seguida de la K y se pulsa la tecla Control seguida de la C (como la combinación de Visual Studio para poner comentarios en la selección que haya en el código). Así que… busqué en internet, pero… había ejemplos muy enrevesados… con temporizadores y demás monadas… así que… basándome en algunos ejemplos (seguramente del mismo autor o copiados unos de otros) he hecho algo que puede servir… al menos a mí me sirve, aunque se puede mejorar, como todo.

    De la forma que lo he hecho da igual si se pulsa primero Ctrl+K que Ctrl+C, ya que lo que he intentado es que se sepa cuando se han hecho esas pulsaciones, y si entre cualquiera de las dos pulsaciones se ha pulsado otra tecla, no se tiene en cuenta esa combinación. Es decir si quieres detectar Ctrl+K seguida de Ctrl+C (que para el caso del código que te mostraré es lo mismo que si pulsas Ctrl+C seguida de Ctrl+K) pulsas otra tecla o combinación de teclas, no se dará como detectada esa doble pulsación.

    Nota:
    Precisamente con esas teclas: Ctrl+C y otras automatizadas de edición: Ctrl+V, Ctrl+X, Ctrl+P, etc., habría que tener cuidado o hacerle un seguimiento distinto al que ahora hago para que no la detecte y, por ejemplo pegue el texto si es Ctrl+P).

    ¿Dónde se hará la comprobación de la tecla pulsada?

    Las comprobaciones de qué tecla se está pulsando (o se ha pulsado) la hago en el evento KeyDown del formulario. Y como de forma predeterminada el formulario no intercepta las pulsaciones de las teclas, habrá que hacer una asignación de un valor verdadero (true) a la propiedad KeyPreview del formulario. Eso lo he puesto en el evento Load, con idea de que esté activado si por casualidad cambio el valor en el diseñador (esas cosas suelen ocurrir, y es complicado de saber porqué antes funcionaba y después no).

    En la figura 1 tienes una captura del código de ejemplo en funcionamiento (en ese caso la aplicación de C# creada con .NET Framework 4.8).

    Figura 1. La aplicación de ejemplo en funcionamiento.

    ¿Cómo saber si hay varias combinaciones de teclas?

    Lo que yo he hecho es crear unas variables para asignarles un valor si la combinación que se quiere detectar se cumple. Esas variables (o campos) definidas a nivel de la clase, las he declarado de tipo entero, (en los ejemplos que vi en la web eran de tipo Boolean, bool en C#), ya que lo que me interesa es saber si algunas de las combinaciones se ha hecho más de una vez, ese es el caso de Ctrl+K, Ctrl+K, es decir: pulsar dos veces la tecla Control y la tecla K.

    Si no quieres comprobar si hay una combinación que se haga más de una vez, lo mismo puedes usar variables de tipo Bolean (bool en C#), eso ya a tu discreción (o preferencia).

    Aquí te muestro el código con la definición de esas variables (tanto para VB como para C#):

    ' Para doble pulsación de teclas
    Private CtrlK As Integer
    Private CtrlC As Integer
    Private CtrlU As Integer
    Private CtrlL As Integer
    Private ShiftAltL As Integer
    Private ShiftAltS As Integer
    
    // Para doble pulsación de teclas
    private int CtrlK;
    private int CtrlC;
    private int CtrlU;
    private int CtrlL;
    private int ShiftAltL;
    private int ShiftAltS;
    

    Esas variables las usaremos en el evento KeyDown del formulario, incrementando el valor cuando se cumpla que se han pulsado las teclas indicadas, por ejemplo si queremos detectar la pulsación de Ctrl+K, tendremos que incrementar el valor de la variable CtrlK, ídem con el resto.

    Como en el ejemplo hay varias combinaciones de teclas que detectar, puede ser un poco largo de ver, pero prefiero que lo veas completo para que no te líes demasiado.
    Ahí se comprueban las tres posibles teclas de «control«, es decir, Control, Shift y Alt. También hago comprobaciones para que, por ejemplo, si queremos detectar Ctrl+Shift se haga en un bloque de código diferente para cuando se detecta, por ejemplo Ctrl+Alt o Shift+Alt.
    Creo que el código está bastante claro y no tendrás complicaciones de ver el proceso que se hace.

    Aquí tienes el código de VB y C#.

    Private Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
            ' Comprobaciones para Ctrl+Shift
    
            ' esta de forma simple
            If e.Control AndAlso e.Shift Then
                If e.KeyCode = Keys.V Then
                    e.Handled = True
    
                    'MostrarRecortes();
                    txtPulsadas.Text = "Capturada: Ctrl+Shift+V" & vbCrLf & txtPulsadas.Text
    
                End If
    
                ' Estas son con varias combinaciones
    
                ' Comprobaciones para Shift+Alt
            ElseIf e.Shift AndAlso e.Alt Then
                ' si se ha pulsado Shift+Alt+L
                If e.KeyCode = Keys.L Then
                    e.Handled = True
    
                    ShiftAltL += 1
                    ' si se ha pulsado Shift+Alt+S
                ElseIf e.KeyCode = Keys.S Then
                    e.Handled = True
    
                    ShiftAltS += 1
                End If
                ' Si se ha pulsado Shitf+Alt+S, Shift+Alt+L
                ' (en cualquier orden)
                If ShiftAltL = 1 AndAlso ShiftAltS = 1 Then
                    e.Handled = True
    
                    'ClasificarSeleccion();
                    txtPulsadas.Text = "Capturada: Shift+Alt+L, Shift+Alt+S" & vbCrLf & txtPulsadas.Text
    
                End If
    
                ' Comprobaciones para solo la tecla Ctrl (sin Shift ni Alt)
            ElseIf e.Control AndAlso Not e.Shift AndAlso Not e.Alt Then
                ' Solo se ha pulsado la tecla Ctrl
                ' comprobar el resto de combinaciones
                ' Forma simple si se ha pulsado Ctrl+B
                If e.KeyCode = Keys.B Then
                    e.Handled = True
    
                    ' Esta solo es para detectar
                    ' la combinación 'simple' de Ctrl+B
                    ' No es necesario llevar la cuenta de las pulsaciones
                    txtPulsadas.Text = "Capturada: Ctrl+B" & vbCrLf & txtPulsadas.Text
    
    
                ElseIf e.KeyCode = Keys.K Then
                    e.Handled = True
    
                    CtrlK += 1
                    txtPulsadas.Text = "Ctrl+K - " & txtPulsadas.Text
    
                ElseIf e.KeyCode = Keys.C Then
                    e.Handled = True
    
                    CtrlC += 1
                    txtPulsadas.Text = "Ctrl+C - " & txtPulsadas.Text
    
                ElseIf e.KeyCode = Keys.U Then
                    e.Handled = True
    
                    CtrlU += 1
                    txtPulsadas.Text = "Ctrl+U - " & txtPulsadas.Text
    
                ElseIf e.KeyCode = Keys.L Then
                    e.Handled = True
    
                    CtrlL += 1
                    txtPulsadas.Text = "Ctrl+L - " & txtPulsadas.Text
    
                End If
    
                ' Si se ha pulsado Ctrl+K, CtrlC
                If CtrlK = 1 AndAlso CtrlC = 1 Then
                    e.Handled = True
    
                    ' Ctrl+K, Ctrl+C
                    CtrlK = 0
                    CtrlC = 0
                    'PonerComentarios(richTextBoxCodigo);
                    txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+C" & vbCrLf & txtPulsadas.Text
    
    
                    ' Si se ha pulsado Ctrl+K, Ctrl+U
                ElseIf CtrlK = 1 AndAlso CtrlU = 1 Then
                    e.Handled = True
    
                    ' Ctrl+K, Ctrl+U
                    CtrlK = 0
                    CtrlU = 0
                    'QuitarComentarios(richTextBoxCodigo);
                    txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+U" & vbCrLf & txtPulsadas.Text
    
    
                    ' Si se ha pulsado Ctrl+K, Ctrl+L
                ElseIf CtrlK = 1 AndAlso CtrlL = 1 Then
                    e.Handled = True
    
                    ' Ctrl+K, Ctrl+L
                    CtrlK = 0
                    CtrlL = 0
                    ' preguntar
                    'buttonEditorMarcadorQuitarTodos.PerformClick();
                    txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+L" & vbCrLf & txtPulsadas.Text
    
    
                    ' Si se ha pulsado Ctrl+K, Ctrl+K
                ElseIf CtrlK = 2 Then
                    e.Handled = True
    
                    ' Ctrl+K, Ctrl+K
                    CtrlK = 0
                    'MarcadorPonerQuitar();
    
                    txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+K" & vbCrLf & txtPulsadas.Text
    
                End If
            Else
                txtPulsadas.Text = $"{vbCrLf}No es una de las teclas comprobadas: {e.KeyCode} +{vbCrLf}" &
                                   $"    Ctrl: {e.Control}, Shift: {e.Shift}, Alt: {e.Alt}{vbCrLf}" & txtPulsadas.Text
    
                CtrlK = 0
                CtrlC = 0
                CtrlU = 0
                ShiftAltL = 0
                ShiftAltS = 0
    
                ' Otras pulsaciones
                ' No están detectadas explícitamente
    
            End If
        End Sub
    
    private void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        // Comprobaciones para Ctrl+Shift
    
        // esta de forma simple
        if (e.Control && e.Shift)
        {
            if (e.KeyCode == Keys.V)
            {
                e.Handled = true;
    
                //MostrarRecortes();
                txtPulsadas.Text = "Capturada: Ctrl+Shift+V\r\n" + txtPulsadas.Text;
    
            }
        }
                
        // Estas son con varias combinaciones
    
        // Comprobaciones para Shift+Alt
        else if (e.Shift && e.Alt)
        {
            // si se ha pulsado Shift+Alt+L
            if (e.KeyCode == Keys.L)
            {
                e.Handled = true;
    
                ShiftAltL += 1;
            }
            // si se ha pulsado Shift+Alt+S
            else if (e.KeyCode == Keys.S)
            {
                e.Handled = true;
    
                ShiftAltS += 1;
            }
            // Si se ha pulsado Shitf+Alt+S, Shift+Alt+L
            // (en cualquier orden)
            if (ShiftAltL == 1 && ShiftAltS == 1)
            {
                e.Handled = true;
    
                //ClasificarSeleccion();
                txtPulsadas.Text = "Capturada: Shift+Alt+L, Shift+Alt+S\r\n" + txtPulsadas.Text;
    
            }
        }
    
        // Comprobaciones para solo la tecla Ctrl (sin Shift ni Alt)
        else if (e.Control && !e.Shift && !e.Alt)
        {
            // Solo se ha pulsado la tecla Ctrl
            // comprobar el resto de combinaciones
    
            // Forma simple si se ha pulsado Ctrl+B
            if (e.KeyCode == Keys.B)
            {
                e.Handled = true;
    
                // Esta solo es para detectar
                // la combinación 'simple' de Ctrl+B
                // No es necesario llevar la cuenta de las pulsaciones
                txtPulsadas.Text = "Capturada: Ctrl+B\r\n" + txtPulsadas.Text;
    
            }
    
            else if (e.KeyCode == Keys.K)
            {
                e.Handled = true;
    
                CtrlK += 1;
                txtPulsadas.Text = "Ctrl+K - " + txtPulsadas.Text;
    
            }
            else if (e.KeyCode == Keys.C)
            {
                e.Handled = true;
    
                CtrlC += 1;
                txtPulsadas.Text = "Ctrl+C - " + txtPulsadas.Text;
    
            }
            else if (e.KeyCode == Keys.U)
            {
                e.Handled = true;
    
                CtrlU += 1;
                txtPulsadas.Text = "Ctrl+U - " + txtPulsadas.Text;
    
            }
            else if (e.KeyCode == Keys.L)
            {
                e.Handled = true;
    
                CtrlL += 1;
                txtPulsadas.Text = "Ctrl+L - " + txtPulsadas.Text;
    
            }
    
            // Si se ha pulsado Ctrl+K, CtrlC
            if (CtrlK == 1 && CtrlC == 1)
            {
                e.Handled = true;
    
                // Ctrl+K, Ctrl+C
                CtrlK = 0;
                CtrlC = 0;
                //PonerComentarios(richTextBoxCodigo);
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+C\r\n" + txtPulsadas.Text;
    
            }
    
            // Si se ha pulsado Ctrl+K, Ctrl+U
            else if (CtrlK == 1 && CtrlU == 1)
            {
                e.Handled = true;
    
                // Ctrl+K, Ctrl+U
                CtrlK = 0;
                CtrlU = 0;
                //QuitarComentarios(richTextBoxCodigo);
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+U\r\n" + txtPulsadas.Text;
    
            }
    
            // Si se ha pulsado Ctrl+K, Ctrl+L
            else if (CtrlK == 1 && CtrlL == 1)
            {
                e.Handled = true;
    
                // Ctrl+K, Ctrl+L
                CtrlK = 0;
                CtrlL = 0;
                // preguntar
                //buttonEditorMarcadorQuitarTodos.PerformClick();
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+L\r\n" + txtPulsadas.Text;
    
            }
    
            // Si se ha pulsado Ctrl+K, Ctrl+K
            else if (CtrlK == 2)
            {
                e.Handled = true;
    
                // Ctrl+K, Ctrl+K
                CtrlK = 0;
                //MarcadorPonerQuitar();
    
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+K\r\n" + txtPulsadas.Text;
    
            }
        }
        else
        {
            txtPulsadas.Text = $"\r\nNo es una de las teclas comprobadas: {e.KeyCode} +\r\n"+
                               $"    Ctrl: {e.Control}, Shift: {e.Shift}, Alt: {e.Alt}\r\n" + txtPulsadas.Text;
            CtrlK = 0;
            CtrlC = 0;
            CtrlU = 0;
            ShiftAltL = 0;
            ShiftAltS = 0;
    
            // Otras pulsaciones
            // No están detectadas explícitamente
        }
    }
    

    Nota:
    En el código están comentadas las funciones / métodos a los que en el programa que uso esta forma de controlar las pulsaciones (múltiples) de teclas llama cuando se produce una. Así sabrás cuándo tienes que actuar cuando se produzca la pulsación esperada.

    Una aclaración sobre la diferencia entre KeyCode y KeyValue

    Hay gente que no se aclara entre los valores de esas dos propiedades del argumento KeyEventArgs del evento KeyDown (o KeyUp).

    KeyCode contiene el código de la tecla pulsada y el del tipo Keys (enumeración).
    KeyValue contiene el código de la tecla pulsada y el de tipo Integer (int en C#).

    En Visual Basic se puede usar indistintamente sin hacer nada especial, es decir, par saber si se ha pulsado la tecla B puedes hacerlo de estas dos formas:
    If e.KeyCode = Keys.B Then o If e.KeyValue = Keys.B

    Pero en C# no te permite hacer la comparación del valor int con un valor de la enumeración Keys. Si así lo quieres hacer, tendrías que hacer un cast al tipo entero:
    if (e.KeyValue == (int)Keys.B)

    Por tanto, es mejor usar e.KeyCode si la intención es compararla con un valor de Keys.

    Ya solo me queda ponerte el código completo de esta aplicación de prueba, pero como últimamente estoy haciendo (creo que solo lo he hecho una vez) ese código (tanto el de Visual Basic como el de C#) está en mis repositorios en GitHub, concretamente en varias-pulsaciones-de-teclas.

    Dicho código está creado en un proyecto para Visual Studio 2019 usando .NET Framework 4.8, pero también es válido para aplicaciones (de WinForms) creadas para .NET Core, al menos yo lo estoy usando en .NET 5.0 RC1.

    Y esto es todo… espero que te sea de utilidad.

    Nos vemos.
    Guillermo