ListView con salto de línea /multilinea

Iniciado por okik, 11 Diciembre 2016, 14:37 PM

0 Miembros y 3 Visitantes están viendo este tema.

okik

hola, me gustaría saber si se puede introducir ítems en un Listbox de modo que se vean verticales

del modo normal sería

12345678
45664555
78999999

Pero yo quiero hacerlo así

1 4 7
2 5 8
3 6 9
4 6 9
5 4 9
6 5 9
7 5 9
8 5 9



Si hago esto por ejemplo...
Código (vbnet) [Seleccionar]
 
       Dim Num() As String = {"1", "2", "3", "4"}
       Dim strLinea As String = Nothing
       strLinea = String.Join(Environment.NewLine, Num).Trim
       ListBox1.Items.Add(strLinea)
       Label1.Text= strLinea


el Label1 muestra correctamente:

1
2
3
4


Mientras que un Listbox o un ListView lo mostraría así
1234

El ListBox aunque lo mostrara como el label pondría uno debajo del otro. Con un Listview podría ponerlo en cada columna, pero también lo pone horizontal.

Lo he hecho con un FlowLayoutPanel1 Panel,  y añado Labels de forma automática pero no es lo que busco. Además que me ocupa mucho código y es tedioso de hacer.

¿Alguna idea?

Gracias




Acebo de darme cuenta que además con Panel, no puedo hacer multiselección

Eleкtro

#1
Hola.

El problema con un ListBox y un ListView es que automaticamente acomodan los caracteres de salto de linea para la vista en horizontal, para representarlo como una sola linea. Me parece que la única solución sería heredar la class ListBox o ListView con OwnerDraw para dibujar manualmente los rectángulos de los items y el contenido de texto, se puede hacer (al menos con un ListView), pero es una solución tediosa que requeriría tiempo y esfuerzo.




Con un ListBox, para hacerlo vertical puedes pasarle un array, esto significa que si tenemos el string "12345678", cada caracter "{1, 2, 3, 4, 5, 6, 7, 8}" será seleccionado por individual, no se si eso te parecerá bien.



Código (vbnet) [Seleccionar]
With ListBox1
   .MultiColumn = True
   .IntegralHeight = False
   .Font = New Font(.Font.FontFamily, 14.0F)
   .ColumnWidth = CInt(Math.Ceiling(.Font.Size))
   .Size = New Size((.ColumnWidth * 4), 200)
End With

Dim arr1 As String() = {"1", "2", "3", "4", "5", "6", "7", "8"}
Dim arr2 As String() = {"4", "5", "6", "6", "4", "5", "5", "5"}
Dim arr3 As String() = {"7", "8", "9", "9", "9", "9", "9", "9"}

ListBox1.Items.AddRange(arr1)
ListBox1.Items.AddRange(arr2)
ListBox1.Items.AddRange(arr3)





Con un ListView, puedes utilizar el modo de vista LargeIcon, sin embargo, para que se muestre en vertical debemos activar la propiedad LabelWrap, y esto nos da un resultado visual poco agradable sobre los items que no están seleccionados, puesto que no podemos redimensionar el tamaño de los rectángulos de cada item a menos que heredemos el control:



Código (vbnet) [Seleccionar]
With ListView1
   .Font = New Font(.Font.FontFamily, 12.25F)
   .View = View.LargeIcon
   .LabelWrap = True
End With

Dim str1 As String = String.Join(ControlChars.Lf, {"1", "2", "3", "4", "5", "6", "7", "8"})
Dim str2 As String = String.Join(ControlChars.Lf, {"4", "5", "6", "6", "4", "5", "5", "5"})
Dim str3 As String = String.Join(ControlChars.Lf, {"7", "8", "9", "9", "9", "9", "9", "9"})

Dim item1 As New ListViewItem(str1)
Dim item2 As New ListViewItem(str2)
Dim item3 As New ListViewItem(str3)

ListView1.Items.AddRange({item1, item2, item3})





Con un DataGridView ocurre exactamente lo mismo por defecto, los caracteres de salto linea se acomodan para la vista en horizontal (las lineas en blanco no se eliminan del item, tampoco en un Listview, simplemente el control representa el texto en las filas sin las lineas en blanco), sin embargo, el DataGridView es un control mucho más personalizable que un ListView, así que podemos adaptarlo a nuestras necesidades de vista de filas en vertical y el resultado quedará bastante bien:



Código (vbnet) [Seleccionar]
With DataGridView1
   .Columns.Add("Column1", "")
   .Columns.Add("Column2", "")
   .Columns.Add("Column3", "")

   .Font = New Font(.Font.FontFamily, 12.25F)
   .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells
   .DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter
   .DefaultCellStyle.WrapMode = DataGridViewTriState.True
End With

Dim str1 As String = String.Join(ControlChars.Lf, {"1", "2", "3", "4", "5", "6", "7", "8"})
Dim str2 As String = String.Join(ControlChars.Lf, {"4", "5", "6", "6", "4", "5", "5", "5"})
Dim str3 As String = String.Join(ControlChars.Lf, {"7", "8", "9", "9", "9", "9", "9", "9"})

DataGridView1.Rows.Add({str1, str2, str3})


En resumen, tu mejor opción es recurrir al control DataGridView, o bien heredar la class ListView, activar el OwnerDraw para dibujar manualmente el contenido del control y trastear con el StringFormat, StringAlignment, y los Bounds para intentar conseguir el resultado de vista en vertical (que no estoy muy seguro de si se podrá, depende de los miembros que sean accesibles]).

Saludos!








okik

Gracias @Elektro

Hay que ver como te curras las respuestas, me sabe mal y todo.

el DataGridView parece una buena opción, lo probaré a ver. No se me había ocurrido probar con él.

La verdad es que no entiendo como no se puede. Como aquí en la primera columna:




Lo intenté con API, con SendMessage, y probando conseguí cosas interesantes pero no eso.


Eleкtro

#3
Cita de: okik en 12 Diciembre 2016, 01:13 AM
La verdad es que no entiendo como no se puede. Como aquí en la primera columna:




Lo intenté con API, con SendMessage, y probando conseguí cosas interesantes pero no eso.

Poder se puede, como ya dije, pero es que lo que muestras en esa imagen, no es un ListView por defecto, en todo cado puede ser un DataGridView modificado, o un ListView que ha sido modificado, es decir, un user-control personalizado. Puedes hacer lo mismo que en esa imagen, como ya dije, heredando la class ListView.

Aquí tienes un ejemplo base:

No necesitas en ningún momento recurrir a las funciones de la API de Windows, eso déjalo para circunstancias en donde no puedas controlarlo de otra forma más directa; al heredar la class ListView, obtienes todo el control necesario, haciendo uso de los miembros heredados, para personalizar la manera en que se renderiza el control (añadir barras de progreso, hacer el texto multi-linea, cambiar los colores por defecto, etc), simplemente aprende a hacerlo:


Pero ya te digo, lo veo una pérdida de tiempo, cuando puedes usar un DataGridView y darle una apariencia similar a un ListView (suponiendo que eso sea lo que te frena).

Saludos!








ivancea96

Esos saltos de línea que ves en la primera columna los genera automáticamente el DataGridView cuando le colocas a la columna DefaultCellStyle->WrapMode = true
Solo existen en la visualización; si agrandases la columna, se recolocaría.

okik

#5
Gracias

El DataGridView va perfecto. No se me ocurrió utilizarlo porque lo usaba para base de datos, modificar tablas .

Código (vbnet) [Seleccionar]

Public Class Form1
    Dim DataGridView1 As New DataGridView
    Sub New()
        ' Llamada necesaria para el diseñador.
        InitializeComponent()
        ' Agregue cualquier inicialización después de la llamada a InitializeComponent().
        Me.Controls.Add(Me.DataGridView1)
    End Sub
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        With DataGridView1
            .DefaultCellStyle.WrapMode = DataGridViewTriState.True
            .RowHeadersVisible = False
            .ColumnHeadersVisible = False
            .ReadOnly = True
            .RowCount = 1
            .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells
            ' .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells
            .MultiSelect = True
            .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText 'Permite copiar el texto
            .RowsDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter 'coloca los valores en el centro
            .Size = New Size(New Point(400, 235))
        End With
        'Configuración de las cabeceras de las columna
        With DataGridView1.ColumnHeadersDefaultCellStyle
            .Font = New Font("Lucida Console Unicode", FontStyle.Bold)
            .Alignment = DataGridViewContentAlignment.MiddleCenter 'coloca el texto en el centro
        End With

        Dim nvars As Integer = 20
        DataGridView1.ColumnCount = nvars
        Task.Factory.StartNew(Sub()
                                  For I As Integer = 0 To nvars - 1
                                      Dim Integ As Integer = I
                                      Me.Invoke(DirectCast(Sub()

                                                               DataGridView1.Columns(Integ).Width = 30
                                                               DataGridView1.Item(Integ, 0).Value = Serie1()

                                                               System.Threading.Thread.Sleep(50)
                                                           End Sub, MethodInvoker))

                                  Next
                              End Sub)

    End Sub
    Public Function Serie1() As String
        Dim col(16) As String
        Dim Rand As New Random
        For index As Integer = 1 To col.Count - 1
            col(index) = CStr(Rand.Next(0, 16))
        Next
        Return CStr(String.Join(Environment.NewLine, col).Trim)
    End Function
End Class





Lo malo es que no parece que pueda evitar que las columnas no sean redimensionables y al mismo tiempo en la creación de la columna establecer el ancho de la misma.



ivancea96

Sí, puedes evitar que sean redimensionables, una a una. Tienes que ir a editar columnas y ahí ves todas sus propiedades.

También tiene el DataGridView unos campos que son los "default", que afectan a todas las columnas, filas y celdas que no tengan un "valor explícito".

Todo lo puedes hacer, échale un ojo a las propiedades detenidamente.

okik

#7
Cita de: ivancea96 en 12 Diciembre 2016, 22:58 PM
Sí, puedes evitar que sean redimensionables, una a una. Tienes que ir a editar columnas y ahí ves todas sus propiedades.

También tiene el DataGridView unos campos que son los "default", que afectan a todas las columnas, filas y celdas que no tengan un "valor explícito".

Todo lo puedes hacer, échale un ojo a las propiedades detenidamente.

claro que puedo evitar que sea redimensionable si ajusto establezco:
DataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells

Pero entonces ya no puedo establecer el ancho, aunque lo haga lo ignora.




Vale, la solución es usar Padding junto con DisplayedCells

Código (vbnet) [Seleccionar]
             
  DataGridView1.DefaultCellStyle.Padding = New Padding(5, 2, 5, 2)
           DataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells


Donde:
Padding(X(izquierdo), Y(Arriba), X(derecho), Y(abajo))

Código (vbnet) [Seleccionar]

Public Class Form1
   Dim DataGridView1 As New DataGridView
   Sub New()
       ' Llamada necesaria para el diseñador.
       InitializeComponent()
       ' Agregue cualquier inicialización después de la llamada a InitializeComponent().
       Me.Controls.Add(Me.DataGridView1)
   End Sub
   Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
       With DataGridView1

           .DefaultCellStyle.WrapMode = DataGridViewTriState.True
           .RowHeadersVisible = False
           .ColumnHeadersVisible = False
           .ReadOnly = True
           .RowCount = 1
           .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells
           .DefaultCellStyle.Padding = New Padding(5, 2, 5, 2)
           .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells
           .MultiSelect = True
           .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText 'Permite copiar el texto
           .RowsDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter 'coloca los valores en el centro
           .Size = New Size(New Point(400, 240))
       End With

       Dim nvars As Integer = 20
       DataGridView1.ColumnCount = nvars
       Task.Factory.StartNew(Sub()
                                 For I As Integer = 0 To nvars - 1
                                     Dim Integ As Integer = I
                                     Me.Invoke(DirectCast(Sub()
                                                              DataGridView1.Item(Integ, 0).Value = Serie1()
                                                              System.Threading.Thread.Sleep(50)
                                                          End Sub, MethodInvoker))

                                 Next
                             End Sub)

   End Sub
   Public Function Serie1() As String
       Dim col(16) As String
       Dim Rand As New Random
       For index As Integer = 1 To col.Count - 1
           col(index) = CStr(Rand.Next(0, 16).ToString("00"))
       Next
       Return CStr(String.Join(Environment.NewLine, col).TrimStart)
   End Function
End Class





Bueno, pues GRACIAS a los dos. Lo doy por solucionado  ;-)

ivancea96

Para evitar usar padding y tener un ancho fijo, le colocas el Width, le colocas Resizable a False y AutoSize a None.

Eleкtro

#9
Conviene que no interactues con el control para construir las columnas y filas, en su lugar puedes definir un DataTable y construir la tabla allí, y entonces usar la propiedad DataSource del DataGridView. Recuerda, cuanta menos interacción directa exista por parte de tu código con los controles, mejor.

He extendido mucho (realmente mucho) el siguiente ejemplo, pero bueno, así es como lo haría yo:

Primeramente, definimos un type para almacenar y/o generar secuencias numéricas aleatorias. Aparte de servirnos para la representación epxlícita de este tipo de secuencias numéricas, otra diferencia (o ventaja) entre una colección List(Of Integer()) y esto, es que podremos reutilizarlo para muchos otros propósitos en el futuro:

Código (vbnet) [Seleccionar]
Public Class RandomSequence

   Protected Shared ReadOnly rand As New Random()

   Public ReadOnly Property Value As Integer()
       Get
           Return Me.valueB
       End Get
   End Property
   Protected valueB As Integer()

   Public ReadOnly Property Value(ByVal format As String) As String
       Get
           Return Me.ToString(New IntegerSequenceFormatter(), format)
       End Get
   End Property

   Public ReadOnly Property Length As Integer
       Get
           Return Me.lengthB
       End Get
   End Property
   Protected lengthB As Integer

   Private Sub New()
   End Sub

   Protected Friend Sub New(ByVal value As Integer())
       Me.valueB = value
       Me.lengthB = value.Length
   End Sub

   Protected Friend Sub New(ByVal length As Integer)
       Me.valueB = Me.GenerateSequence(length).ToArray()
       Me.lengthB = length
   End Sub

   Protected Friend Sub New(ByVal length As Integer, ByVal minValue As Integer, ByVal maxValue As Integer)
       Me.valueB = Me.GenerateSequenceInternal(length, minValue, maxValue).ToArray()
       Me.lengthB = length
   End Sub

   Public Overridable Function GenerateSequence(ByVal length As Integer) As Integer()
       Me.valueB = Me.GenerateSequenceInternal(length, minValue:=0, maxValue:=Integer.MaxValue).ToArray()
       Return Me.valueB
   End Function

   Public Overridable Function GenerateSequence(ByVal length As Integer, ByVal minValue As Integer, ByVal maxValue As Integer) As Integer()
       Me.valueB = Me.GenerateSequenceInternal(length, minValue, maxValue).ToArray()
       Return Me.valueB
   End Function

   Protected Iterator Function GenerateSequenceInternal(ByVal length As Integer, ByVal minValue As Integer, ByVal maxValue As Integer) As IEnumerable(Of Integer)
       For i As Integer = 0 To (length - 1)
           Yield rand.Next(minValue, maxValue)
       Next i
   End Function

   Public Overrides Function Equals(ByVal obj As Object) As Boolean
       If (TypeOf obj IsNot RandomSequence) Then
           Return False
       Else
           Return String.Join(" ", Me.valueB).Equals(String.Join(" ", DirectCast(obj, RandomSequence).valueB))
       End If
   End Function

   Public Overridable Shadows Function ToString(ByVal formatProvider As IFormatProvider, ByVal format As String) As String
       If (TypeOf formatProvider Is ICustomFormatter) Then
           Return DirectCast(formatProvider, ICustomFormatter).Format(format, Me.valueB, formatProvider)

       ElseIf (formatProvider IsNot Nothing) Then
           Return formatProvider.ToString()

       Else
           Return Me.valueB.ToString()

       End If
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   Public Overridable Shadows Function GetHashCode() As Integer
       Return MyBase.GetHashCode()
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   Public Overridable Shadows Function [GetType]() As Type
       Return MyBase.GetType()
   End Function

   <EditorBrowsable(EditorBrowsableState.Never)>
   Public Shared Shadows Function ReferenceEquals(ByVal objA As Object, ByVal objB As Object) As Boolean
       Return Object.ReferenceEquals(objA, objB)
   End Function

End Class


Ejemplo de uso:
Código (vbnet) [Seleccionar]
Dim seq1 As New RandomSequence(length:=3) ' minValue:=0, maxValue:=Integer.MaxValue
Dim seq2 As New RandomSequence(length:=3, minValue:=0, maxValue:=100)
Dim seq3 As New RandomSequence({1, 2, 3})

Console.WriteLine(seq1.Length)
Console.WriteLine(seq1.Equals(seq2))
Console.WriteLine(seq1.Value.ToString())
Console.WriteLine(seq1.Value("Horizontal")) ' o "H"
Console.WriteLine(seq1.Value("Vertical"))   ' o "V"





Seguidamente, definimos un segunto Type donde implementaremos las interfaces IFormatProvider y ICustomFormatter, y así desarrollaremos el algoritmo de formateo para representar en texto horizontal o vertical una secuencia numérica. La gran ventaja de desarrollar nuestro propio proveedor de formato es que se puede adaptar para utilizarlo de mil formas distintas, en cientos de situaciones diferentes.

Código (vbnet) [Seleccionar]
Public Class IntegerSequenceFormatter : Implements IFormatProvider, ICustomFormatter

   Public Function GetFormat(formatType As Type) As Object Implements IFormatProvider.GetFormat
       If formatType Is GetType(ICustomFormatter) Then
           Return Me
       Else
           Return Nothing
       End If
   End Function

   Public Function Format(fmt As String, arg As Object, formatProvider As IFormatProvider) As String Implements ICustomFormatter.Format

       If Not Me.Equals(formatProvider) Then
           Return Nothing
       End If

       If String.IsNullOrEmpty(fmt) Then
           Dim result As Integer() = TryCast(arg, Integer())
           If (result Is Nothing) Then
               Throw New ArgumentException("Value is not of type Integer()", paramName:="arg")
               Exit Function
           End If
       End If

       If String.IsNullOrEmpty(fmt) Then
           Throw New ArgumentNullException(paramName:="fmt")
           Exit Function
       End If

       Select Case fmt.ToLower()
           Case "h", "horizontal"
               Dim seq As Integer() = DirectCast(arg, Integer())
               Dim maxLength As Integer = CStr(seq.Max()).Length
               Dim sb As New StringBuilder(capacity:=seq.Length * maxLength)
               For Each item As Integer In seq
                   sb.Append(item.ToString().PadLeft(maxLength, "0"c))
                   sb.Append(" "c)
               Next
               Return sb.ToString.TrimEnd(" "c)

           Case "v", "vertical"
               Dim seq As Integer() = DirectCast(arg, Integer())
               Dim maxLength As Integer = CStr(seq.Max()).Length
               Dim sb As New StringBuilder(capacity:=seq.Length * maxLength)
               For Each item As Integer In seq
                   sb.AppendLine(item.ToString().PadLeft(maxLength, "0"c))
               Next
               Return sb.ToString()

           Case Else
               Throw New FormatException(String.Format("'{0}' cannot be used to format {1}.", fmt, arg.ToString()))
       End Select

   End Function

End Class


Ejemplo de uso:
Código (vbnet) [Seleccionar]
Dim arr As Integer() = {1, 2, 3, 4, 5}
Dim strHorz As String = String.Format(New IntegerSequenceFormatter, "{0:H}", arr)
Dim strVert As String = String.Format(New IntegerSequenceFormatter, "{0:V}", arr)

Console.WriteLine(strHorz)
Console.WriteLine(strVert)


Nota:
En el el type RandomSequence no necesitaremos utilizarlo pasando tantos argumentos, lo acortaremos a RandomSequence.Value("H") y RandomSequence.Value("V").




Por último, solo nos queda hacer uso de todo esto para construir la tabla y representarla en el control DataGridView:

Código (vbnet) [Seleccionar]
Public Class Form1 : Inherits Form

   Dim seqList As New List(Of RandomSequence)
   Dim seqTable As New DataTable("RandomSequenceTable")

   Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

       Dim seqCount As Integer = 20 ' Amount of random sequences to generate.

       ' Build random sequences list. This is optional, just to have a short reference to a generic collection in source-code.
       For i As Integer = 0 To (seqCount - 1)
           seqList.Add(New RandomSequence(length:=16, minValue:=0, maxValue:=16))
       Next

       ' Build table from list.
       For x As Integer = 0 To (seqList.Count - 1)
           seqTable.Columns.Add(New DataColumn)
           seqTable.Columns(x).DataType = GetType(RandomSequence)
       Next
       seqTable.Rows.Add.ItemArray = seqList.ToArray()
       ' To build a data-table of vertical strings representation:
       ' seqTable.Rows.Add.ItemArray = (From seq As RandomSequence In seqList Select seq.Value("V")).ToArray()
       seqTable.AcceptChanges()

       ' Build grid.
       DataGridView1.SuspendLayout()
       With DataGridView1
           .AllowUserToAddRows = False
           .AllowUserToResizeColumns = False
           .AutoGenerateColumns = True
           .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells
           .AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells
           .ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText
           .ColumnHeadersVisible = False
           .DefaultCellStyle.Padding = New Padding(5, 2, 5, 2)
           .DefaultCellStyle.WrapMode = DataGridViewTriState.True
           .MultiSelect = True
           .ReadOnly = True
           .RowHeadersVisible = False
           .RowsDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter
           .Size = New Size(New Point(400, 240))
       End With
       DataGridView1.DataSource = seqTable
       DataGridView1.ResumeLayout()

   End Sub

   Private Sub DataGridView1_CellFormatting(ByVal sender As Object, ByVal e As DataGridViewCellFormattingEventArgs) _
   Handles DataGridView1.CellFormatting

       If (e.Value IsNot Nothing) Then
           Dim item As RandomSequence = TryCast(e.Value, RandomSequence)
           If (item IsNot Nothing) Then
               e.Value = item.Value("Vertical")
           End If
       End If

   End Sub

End Class


Nótese que el contenido de las celdas se formatean controlando el evento DataGridView1.CellFormatting, esto es algo ilustrativo y opcional, se puede contruir la data-table con las celdas ya formateadas para no tener que hacerlo después controlando ese evento, pero veo de mayor utilidad tener un data-table de RandomSequence, que de Strings multilinea...




Resultado de ejecución:



Saludos!