Problème de copie du buffer d'une console

Signaler
Messages postés
133
Date d'inscription
dimanche 20 mai 2007
Statut
Membre
Dernière intervention
13 juillet 2012
-
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
-
Salut à tous !
Cela faisait un petit moment que je ne m'étais pas connecté !!!

Un problème s'offre à moi aujourd'hui :
J'essaie de copier le buffer (ou une partie) de ma console, mais sans succès.
Soit cela ne marche pas du tout soit j'obtiens une ExecutionEngineException à l'exécution de l'API ReadConsoleOutput.

Voici mon bout de code :

Module Main

   Private Const STD_OUTPUT_HANDLE = -11

   <DllImport( _
   "kernel32.dll", EntryPoint:="GetStdHandle", SetLastError:=True)> _
   Private Function API_GetStdHandle(ByVal nStdHandle%) As IntPtr

   ' Normalement lpBuffer est un pointeur qui cible le 1er élément d'un
   ' tableau bi-dimensionnel de structures CHAR_INFO. Je pense que c'est
   ' bon mais pas certain.
   <DllImport( _
   "kernel32.dll", EntryPoint:="ReadConsoleOutputA", SetLastError:=True)> _
   Private Function API_ReadConsoleOutput( _
   ByVal hConsoleOutput As IntPtr, _
   ByRef lpBuffer(,) As CHAR_INFO, _
   ByVal dwBufferSize As COORD, _
   ByVal dwBufferCoord As COORD, _
   ByRef lpReadRegion As SMALL_RECT) As Boolean
   End Function

   <StructLayout(LayoutKind.Sequential)> _
   Private Structure CHAR_INFO
      Private charData As UShort
      Private attributes As Short
   End Structure

   <StructLayout(LayoutKind.Sequential)> _
   Private Structure COORD
      Friend X As Short
      Friend Y As Short
   End Structure

   <StructLayout(LayoutKind.Sequential)> _
   Private Structure SMALL_RECT
      Friend Left As Short
      Friend Top As Short
      Friend Right As Short
      Friend Bottom As Short
   End Structure

   <MarshalAs(UnmanagedType.LPArray)> _
   Dim lpBuffer(3, 3) As CHAR_INFO

   Sub Main()

      Dim dwBufferSize As COORD
      dwBufferSize.X = 3
      dwBufferSize.Y = 3
      Dim dwBufferCoord As COORD
      dwBufferCoord.X = 0
      dwBufferCoord.Y = 0
      Dim lpReadRegion As SMALL_RECT
      lpReadRegion.Left = 0
      lpReadRegion.Top = 0
      lpReadRegion.Right = 3
      lpReadRegion.Bottom = 3
      Dim hConsole As IntPtr = API_GetStdHandle(STD_OUTPUT_HANDLE)

      For i = 0 To 10 : WriteLine("Line number " & i) : Next

      For i = 0 To 3
         For j 0 To 3 : lpBuffer(i, j) New CHAR_INFO : Next
      Next

      Dim res = API_ReadConsoleOutput(hConsole, lpBuffer, _
      dwBufferSize, dwBufferCoord, lpReadRegion)

   End Sub

End Module


Je vous remercie d'avance pour votre aide, cela m'enlèverait une grosse épine du pied !
++

12 réponses

Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Bonjour êtes-vous sur de votre structure ChAR_INFO ?
typedef struct _CHAR_INFO {
  union {
    WCHAR UnicodeChar;
    CHAR  AsciiChar;
  } Char;
  WORD Attributes;
} CHAR_INFO, *PCHAR_INFO;


http://msdn.microsoft.com/en-us/library/ms682013(v=VS.85).aspx

la variable char est une union d'un caractère UNICODE et d'un cartère ANSI. Je je ne suis pas sur que ca se traduise par un UShort.

C'est peut-être une piste.

Peut-être aussi le lpBuffer qui dépasse les 64K
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Je retire l'option 64K, je n'avais pas fait attention au code de test
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Avec une peut plus de précision je dirai même
<MarshalAs(UnmanagedType.LPArray)> _
    Public lpBuffer(20, 20) As CHAR_INFO

    Sub Main()
        Dim dwBufferSize As COORD
        dwBufferSize.X = 21
        dwBufferSize.Y = 21
        Dim dwBufferCoord As COORD
        dwBufferCoord.X = 0
        dwBufferCoord.Y = 0
        Dim lpReadRegion As SMALL_RECT
        lpReadRegion.Left = 0
        lpReadRegion.Top = 0
        lpReadRegion.Right = 50
        lpReadRegion.Bottom = 50
        Dim hConsole As IntPtr = API_GetStdHandle(STD_OUTPUT_HANDLE)

        For i As Integer = 0 To 10
            Console.WriteLine("Line number " & i)
        Next

        For i As Integer = 0 To 20
            For j As Integer = 0 To 20
                lpBuffer(i, j) = New CHAR_INFO
            Next
        Next

        Dim res = API_ReadConsoleOutput(hConsole, lpBuffer, dwBufferSize, dwBufferCoord, lpReadRegion)
    End Sub


A première vue je dirai que le buffer n'était pas assez grand ainsi que la région.

Une chose est sur, si vous déclarez "Public lpBuffer(20, 20) As CHAR_INFO" le buffersize doit faire "21;21". (0 à 20 = 21 lignes ou colonnes).

Je n'ai pas controlé le résultat mais ca élimine l'erreur
Messages postés
133
Date d'inscription
dimanche 20 mai 2007
Statut
Membre
Dernière intervention
13 juillet 2012
1
Salut foliv !

Je te remercie de te pencher sur le problème !
J'avais lu cette doc et c'est vrai que c'est étrange ce "union" (je ne connaissais pas), mais je pense que ma structure CHAR_INFO est bonne.

Pour la taille du buffer, c'est juste, mais en réalité cela n'a pas d'importance car la fonction ReadConsoleOutput() copie ce qu'elle peut :

[i]ReadConsoleOutput treats the console screen buffer and the destination buffer as two-dimensional arrays (columns and rows of character cells). The rectangle pointed to by the lpReadRegion parameter specifies the size and location of the block to be read from the console screen buffer. A destination rectangle of the same size is located with its upper-left cell at the coordinates of the dwBufferCoord parameter in the lpBuffer array. Data read from the cells in the console screen buffer source rectangle is copied to the corresponding cells in the destination buffer. If the corresponding cell is outside the boundaries of the destination buffer rectangle (whose dimensions are specified by the dwBufferSize parameter), the data is not copied.

Cells in the destination buffer corresponding to coordinates that are not within the boundaries of the console screen buffer are left unchanged. In other words, these are the cells for which no screen buffer data is available to be read.

Before ReadConsoleOutput returns, it sets the members of the structure pointed to by the lpReadRegion parameter to the actual screen buffer rectangle whose cells were copied into the destination buffer. This rectangle reflects the cells in the source rectangle for which there existed a corresponding cell in the destination buffer, because ReadConsoleOutput clips the dimensions of the source rectangle to fit the boundaries of the console screen buffer.

If the rectangle specified by lpReadRegion lies completely outside the boundaries of the console screen buffer, or if the corresponding rectangle is positioned completely outside the boundaries of the destination buffer, no data is copied. In this case, the function returns with the members of the structure pointed to by the lpReadRegion parameter set such that the Right member is less than the Left, or the Bottom member is less than the Top. To determine the size of the console screen buffer, use the GetConsoleScreenBufferInfo function./i

Je ne sais pas pourquoi, mais j'ai l'impression que GetStdHandle() ne me retourne pas le bon handle...
Au mieux mon lpBuffer contient 1 seule structure CHAR_INFO après retour, mais jamais un tableau... Peut être mon <MarshalAs(UnmanagedType.LPArray)> est-il incorrect ? Je ne comprends pas !

Si vous avez d'autres idées.... Je suis preneur, car complètement bloqué !
Merci encore
++
Messages postés
3275
Date d'inscription
jeudi 3 avril 2008
Statut
Membre
Dernière intervention
14 septembre 2014
4
Bonjour,

si tu veux lire une console que tu démmarres toi même avec Process.Start tu peux rediriger la sortie vers une textbox ou autre http://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput.aspx

a+
google est mon ami quand tu cherches quelque chose demande lui clairement
Messages postés
133
Date d'inscription
dimanche 20 mai 2007
Statut
Membre
Dernière intervention
13 juillet 2012
1
Salut gillardg !

Oui je sais ça mais c'est pas le but....
En réalité mon appli est de type console et je veux pouvoir faire une "sauvegarde" de l'écran de la console d'origine (du buffer quoi) dans laquelle mon appli se lance.

Euh... je sais pas si c'est compréhensible ?

Mais ça y est mon code tourne mieux, le fonctionnement n'est pas encore clair à 100% dans ma tête mais au moins j'arrive à copier le buffer.

Ce que je ne comprends pas trop c'est qu'apparemment lpBuffer doit pointer vers un tableau faisant moins de 64K (d'après la doc msdn). Je ne sais pas trop ce qu'ils veulent dire par là, 65536o en mémoire ? Si c'est ça je sais pas trop comment savoir quelle taille a mon tableau, j'ai bien fais 2/3 calculs mais rien ne correspond sachant que pour lpReadRegion=80*101 ça fonctionne mais pour lpReadRegion=80*102 ça coince...

Des idées ?
++
Messages postés
3275
Date d'inscription
jeudi 3 avril 2008
Statut
Membre
Dernière intervention
14 septembre 2014
4
Bonjour,

heu pourquoi pas faire un log ??? ce que tu envoies sur la console tu sais aussi le copier dans un fichier , sans trop te fatiguer
a+
google est mon ami quand tu cherches quelque chose demande lui clairement
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Pour le handle, votre application étant une application console, peut être qu'un "Me.Handle" devrait suffir. (Je me trompe peut-être).

Pour le marshaling de tableau à deux dimensions, je n'avais jamais poussé aussi loin, surtout en output.

Je vais faire des essais et vous tiens au courant
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Et voila :
Module Module1

    Public Const STD_INPUT_HANDLE As Integer = -10
    Public Const STD_OUTPUT_HANDLE As Integer = -11
    Public Const STD_ERROR_HANDLE As Integer = -12

    Public Enum ConsoleStandardDevice
        Input = STD_INPUT_HANDLE
        Output = STD_OUTPUT_HANDLE
        Errorc = STD_ERROR_HANDLE
    End Enum



    <DllImport( _
    "kernel32.dll", EntryPoint:="GetStdHandle", SetLastError:=True)> _
    Private Function API_GetStdHandle(<MarshalAs(UnmanagedType.I4)> ByVal nStdHandle As Integer) As IntPtr
    End Function

    ' Normalement lpBuffer est un pointeur qui cible le 1er élément d'un
    ' tableau bi-dimensionnel de structures CHAR_INFO. Je pense que c'est
    ' bon mais pas certain.
    <DllImport( _
    "kernel32.dll", EntryPoint:="ReadConsoleOutputA", SetLastError:=True)> _
    Private Function API_ReadConsoleOutput( _
         ByVal hConsoleOutput As IntPtr, _
         ByRef lpBuffer As CHAR_INFO, _
         ByVal dwBufferSize As COORD, _
         ByVal dwBufferCoord As COORD, _
         ByRef lpReadRegion As SMALL_RECT) As Boolean
    End Function

    <StructLayout(LayoutKind.Explicit, CharSet:=CharSet.Auto)> _
    Private Structure CHAR_TYPE
        <FieldOffset(0)> _
        Public UnicodeChar As Short

        <FieldOffset(0)> _
        Public AsciiChar As Byte
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure CHAR_INFO
        Public charData As CHAR_TYPE
        <MarshalAs(UnmanagedType.U2)> Public attributes As Short
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure COORD
        <MarshalAs(UnmanagedType.I2)> Public X As Short
        <MarshalAs(UnmanagedType.I2)> Public Y As Short
    End Structure

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure SMALL_RECT
        <MarshalAs(UnmanagedType.I2)> Public Left As Short
        <MarshalAs(UnmanagedType.I2)> Public Top As Short
        <MarshalAs(UnmanagedType.I2)> Public Right As Short
        <MarshalAs(UnmanagedType.I2)> Public Bottom As Short
    End Structure

    Sub Main()

        Dim lpBuffer(51) As CHAR_INFO
        Dim dwBufferSize As COORD
        dwBufferSize.X = 13
        dwBufferSize.Y = 4
        Dim dwBufferCoord As COORD
        dwBufferCoord.X = 0
        dwBufferCoord.Y = 0
        Dim lpReadRegion As SMALL_RECT
        lpReadRegion.Left = 0
        lpReadRegion.Top = 0
        lpReadRegion.Right = 15
        lpReadRegion.Bottom = 4
        Dim hConsole As IntPtr = API_GetStdHandle(ConsoleStandardDevice.Output)

        For i As Integer = 1 To 4
            Console.WriteLine("Line number " & i)
        Next

        Try
            Dim res = API_ReadConsoleOutput(hConsole, lpBuffer(0), dwBufferSize, dwBufferCoord, lpReadRegion)

            For i As Integer = 0 To lpBuffer.GetUpperBound(0)
                Console.WriteLine("{0} | {1}", lpBuffer(i).charData.AsciiChar, lpBuffer(i).attributes)
            Next
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try

        Console.ReadKey(True)

    End Sub


End Module


La structure CHAR_INFO était bien incorrecte.

Le buffer est un tableau de CHAR_INFO a 1 dimension. Je m'étais aussi fait avoir en lisant l'aide. C'est bizard ...

Il faut que ca taille corresponde au nombre de caractères lus (dwBufferSize (13,4) 13 caractères par lignes sur 4 lignes soit 13*4 52 caractères)
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
J'ai dit une betise. Ca marche très bien avec un tableau a 2 dimentions.

Il faut juste passer en paramètre le premier élément du tableau

Sub Main()

        Dim lpBuffer(12, 3) As CHAR_INFO
        Dim dwBufferSize As COORD
        dwBufferSize.X = 13
        dwBufferSize.Y = 4
        Dim dwBufferCoord As COORD
        dwBufferCoord.X = 0
        dwBufferCoord.Y = 0
        Dim lpReadRegion As SMALL_RECT
        lpReadRegion.Left = 0
        lpReadRegion.Top = 0
        lpReadRegion.Right = 15
        lpReadRegion.Bottom = 4
        Dim hConsole As IntPtr = API_GetStdHandle(ConsoleStandardDevice.Output)

        For i As Integer = 1 To 4
            Console.WriteLine("Line number " & i)
        Next

        Try
            Dim res = API_ReadConsoleOutput(hConsole, lpBuffer(0, 0), dwBufferSize, dwBufferCoord, lpReadRegion)

            For i As Integer = 0 To lpBuffer.GetUpperBound(0)
                For j As Integer = 0 To lpBuffer.GetUpperBound(1)
                    Console.WriteLine("{0} | {1}", Chr(lpBuffer(i, j).charData.AsciiChar), lpBuffer(i, j).attributes)
                Next
            Next
        Catch ex As Exception
            Console.WriteLine(ex.Message)
        End Try

        Console.ReadKey(True)

    End Sub
Messages postés
133
Date d'inscription
dimanche 20 mai 2007
Statut
Membre
Dernière intervention
13 juillet 2012
1
>>> Gillardg je te remercie pour ces astuces, mais je DOIS copier le buffer de ma console. Sinon si le but était juste de sauver le texte de ma sortie j'aurais évidemment fais plus simple...

>>> foliv, je vois que le sujet t'emballe, tant mieux ! Et merci pour tes recherches ! Mon code a bien évolué et fonctionne désormais (enfin la base). Un rapide coup d'œil à ton code me fait dire que l'on a suivi la même piste. (J'ai également créé une structure pour ascii/unicode et passé lpBuffer(0,0) en param, par contre j'ai pas marshallé aussi précisément que toi.)

J'ai toujours un problème pour savoir si la taille de ma lpReadRegion est excessive ou non... je sais juste que ça fonctionne (d'après tests) jusqu'à environ 8000 "cellules".

Comme il y a cette limite de copie, cela m'amène à un nouveau souci, je vais devoir faire une boucle pour copier intégralement le buffer. L'inconvénient, c'est que le buffer peut faire 9999x9999 et dans ce cas 1 seule ligne dépassera le nombre de cellules copiables par appel... Pas impossible, mais ça risque d'être ch**** à faire !

J'étudie plus en détail ton code voir si il y a des améliorations que j'peux te piquer et je vous tiens au jus...

++
Messages postés
420
Date d'inscription
vendredi 17 novembre 2006
Statut
Membre
Dernière intervention
15 juillet 2014
5
Je ne vois pas bien ce que vous voulez dire par 8000 cellules et le buffer de 9999x9999.

La console peut contenir 80 caratères sur 300 lignes avant d'effacer les premières. (Qu'on peut retrouver grace à la fonction GetConsoleScreenBufferInfo)

Il est possible de monter le nombre de CHAR_INFO, contenu dans le buffer de retour de la fonction, jusqu'à 13304.

1 CHAR_INFO faisant 4 octets : 13304 * 4 53216 o> 52Ko (Par contre la je ne comprends pas pourquoi je n'arrive pas à atteindre les 64Ko donnée par la doc ...)

donc il sera possible de de faire une lecture avec les paramètres suivants :
Dim lpBuffer(165, 79) As CHAR_INFO
Dim dwBufferSize As COORD
dwBufferSize.X = 80
dwBufferSize.Y = 166
Dim lpReadRegion As SMALL_RECT
lpReadRegion.Left = 0
lpReadRegion.Top = 0
lpReadRegion.Right = 80
lpReadRegion.Bottom = 300


Donc lecture complète de 166 lignes.
Au retour de la fonction le lpReadRegion vous donera la region réelement lue (soit 79 en right et 165 en bottom)

Vous pourrez donc vous baser sur ca pour lire la region suivante (Soit left=0 top=166 right=80 bottom=466)