Acerca de:

Este blog contiene los códigos, ejemplos y bases de datos que he usado cuando aprendía acerca de algún tema específico. En lugar de borrarlos (una vez dominado ya el tema), he decidido publicarlos :)

miércoles, 21 de diciembre de 2011

Mi Repositorio de Códigos Fuente

Son códigos que ya he publicado anteriormente, pero ahora en un sólo lugar, en mi cuenta en mygnet para que todo el mundo los descargue, los copie, los modifique, los analice, los traduzca a otros lenguajes, y haga todo lo que les plazca con ellos :D

jueves, 17 de noviembre de 2011

La Api ShellExecute devuelve código de error 42

El error 42 se produce cuando se intenta abrir un ejecutable con la api ShellExecute, la cual sirve para abrir archivos o carpetas. En el caso de VB6, si se desea ejecutar otra aplicación se deberá usar la función Shell.

En VB6 la api ShellExecute se declara como:

Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal HWnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long


Si deseo abrir un archivo con el programa que lo abre por defecto, deberé escribir (Me es el formulario):
Dim ret As Long
ret = ShellExecute(Me.HWnd, "Open", "C:\text.txt", "", "", 1)

Esto abrirá el archivo de texto con el block de notas.

Si deseo abrir un archivo con otro programa, escribiré:
Dim ret As Long
ret = ShellExecute(Me.HWnd, "Open", "Wordpad.exe", "C:\text.txt", "", 1)

Lo cual abrirá el archivo de texto con el Wordpad de Windows.

La función Shell es propia de VB6. Si quiero ejecutar una aplicación deberé escribir:
Shell "C:\MiApp.exe", vbNormalFocus

Update Abril 2013:

Sí se pueden ejecutar archivos ejecutables con la api ShellExecute, sólo hay que pasarle los parámetros correctos:

Dim ret As Long
ret = ShellExecute(Me.hwnd, vbNullString, _ 
"D:\unaCarpeta\Otra Carpeta\mIEjecutable.exe", vbNullString, vbNullString, 1)

Si no se va a evaluar el valor devuelto por ShellExecute no es necesario declarar la variable ret. Por ejemplo esto ejecuta el block de notas:
ShellExecute Me.hwnd, vbNullString, "notepad.exe", vbNullString, vbNullString, 1

Y esto ejecuta la calculadora de windows:
ShellExecute Me.hwnd, vbNullString, "calc.exe", vbNullString, vbNullString, 1

lunes, 24 de octubre de 2011

Error al actualizar un Recordset: "El Campo no es Actualizable"

Intentaba modificar un registro de una base de datos en Access desde VB6, cuando, al hacer miRecordset.Update, me saltaba el siguiente error:


En general, el mensaje de error es: "No se puede actualizar 'nombre del campo'; el campo no es actualizable".

Esto se debe a que ese campo específico tiene alguna característica que hace que no se puede reescribir. En mi base d edatos, el campo "Id" es llave primaria y es auto incrementable. 

Aunque los permisos de la base de datos permitan hacer modificaciones a sus datos, un campo del recordset no permite cambiar su contenido si es auto incrementable. Antes de actualizar mi recordset, necesitaba saber qué campos son autoincrementables para no tocarlos cuando quiera modificar su contenido.

Después de mucho buscar por internet, di con este foro:
http://www.experts-exchange.com/Programming/Languages/Visual_Basic/Q_20248270.html

Son rescatables estos posts:

Expert Comment

Posted by rspahitz on 21/12/01 at 8:20 AM
I don't know the answer, but since (I think) Properties is a collection, you should be able to cycle through and find the valid choices:

for i = 0 to rs.Fields(x).Properties.count - 1
   debug.print rs.Fields(x).Properties(i).name
next i



Author Comment

Posted by andyknott on 21/12/01 at 8:28 AM
Hi rspahitz,

Thanks that helped a bit, the properties listed are

BASECATALOGNAME
BASECOLUMNNAME
BASESCHEMANAME
BASETABLENAME
COLLATINGSEQUENCE
COMPUTEMODE
DATETIMEPRECISION
ISAUTOINCREMENT
ISCASESENSITIVE
ISSEARCHABLE
OCTETLENGTH
KEYCOLUMN
COMPFLAGS
SORTID
BASETABLEINSTANCE
TDSCOLLATION



Para saber, por ejemplo, si un campo con índice x es auto incrementable, se hace:
If miRecordset(x).Properties("IsAutoIncrement").Value Then _
Debug.Print "Es Auto Incrementable"

En mi caso, al modificar mi recordset, hago:
For i = 0 To miRecordset.Fields.count - 1
    If Not miRecordset(i).Properties("IsAutoIncrement").Value Then
        miRecordset(i) = nuevoValor
    End If
Next

miRecordset.Update

Con esto elimino el error de Campo no Actualizable.

sábado, 8 de octubre de 2011

Desocultando Carpetas Ocultadas por Virus con Linq

Actualización de http://electronicaamartillazos.co.nr/ (sección C#) donde muestro cómo usar Linq para crear un programita que desoculta todas las carpetas ocultas (y declaradas como carpetas de sistema) en los directorios raíz de discos duros y memorias extraíbles. Los virus que ocultan carpetas también las declaran como carpetas de sistema, de modo que no se pueden desocultar manualmente.
Este programita sirve como base para hacer programas más complejos que administren los atributos de archivos, carpetas y subcarpetas.

viernes, 9 de septiembre de 2011

Error en Crystal Reports: "Esta sección de Grupo no se puede imprimir..."

El mensaje completo es:
"Esta Seccion de grupo no se puede imprimir porque su campo de condicion no existe o no es valido. Dar formato a la seccion para elegir otro campo de condicion". Este mensaje de error aparece al querer ir a la pestaña "Vista Previa". Al presionar el botón Aceptar, aparece una página en blanco en lugar del reporte.


Busqué en Internet y no hallé cómo eliminar este error. Tampoco hallé nada útil en la Ayuda de Crystal Reports. Pero, después de probar un poco, dí yo misma con la solución.

Todos los reportes de Crystal Reports van conectados a una Base de Datos. Los grupos del reporte (por ejemplo un encabezado), están vinculadas a un campo específico de la Base de Datos. Esto es para que Crystal Reports sepa en qué orden mostrar los reportes con la data jalada desde la Base de Datos.
Este error se genera al cambiar la conexión a otra Base de Datos, eliminarla, cambiar a otra tabla, y cualquier situación en que el reporte no tenga cómo hallar el campo al que estaba vinculado.

Para solucionar esto, se debe ir al asistente de Base de Datos y configurar o crear la conexión, esto es necesario en caso de haberla eliminado:



Luego, en la pertaña Diseño, le hacemos click derecho al grupo que da problemas y elegimos la opción "Cambiar el Grupo":


Se nos abrirà la ventana que nos permitirá asignar un nuevo campo de condición:


Con esto, ya podemos ir a la Vista Previa de nuestro reporte :D

lunes, 1 de agosto de 2011

Leyendo data numérica desde un archivo de texto con C++

Tengo un archivo de texto que contiene una lista de números con el siguiente formato:


He llamado al archivo "18.txt" por ninguna razón en especial. Lo que quiero hacer es leer los números en el archivo y realizar algunas operaciones matemáticas con ellos. Como están en formato de texto, primero debo convertirlos a números decimales.
Para ello uso la función "atof", pero hay un problema: mis números usan coma decimal, y la función "atof" considera punto decimal. Debo leer los números, línea por línea, y  ponerlos en el formato con el que puede trabajar "atof", así que declaro un aray de caracteres llamado "palabra". Ninguno de los números guardados en "18.txt" ocupa más de cinco caracteres, así que "palabra" es declarada como: char palabra[6];
Luego recorro cada caracter dentro de "palabra" y si encuentro una coma, la reemplazo por un punto.

El array "beta", separa espacio en memoria para 150 números decimales ya que sólo quiero operar con 150 números en total.

El código fuente es el siguiente (el ejecutable compilado está en la misma carpeta que "18.txt"):

#include <iostream.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#pragma hdrstop
#pragma argsused

void main(int argc, char* argv[])
{    FILE *fp;
     int i;
     double *beta, c, promedio, rms, desviacion;
     char palabra[6];

     beta = (double *)malloc(sizeof(double)*151);
     i=0;
     fp = fopen("18.txt", "r");

if (fp==NULL)
 cout<<"no existe el archivo \n";

else {

    do
    {
       c = fscanf(fp, "%s", palabra);

       for(int j=0; j<6; j++)
       if (palabra[j]== ',')
            { palabra[j]='.' break; }
       
        beta[i] = atof(palabra);
        cout<<beta[i];
        cout<<"\n";
        i++;
    }
  while (c != EOF && i != 151);

//valores estadísticos:
promedio=0; desviacion=0; rms=0;

//Promedio
     for (int j= 0; j<=150; j++)
       promedio= promedio+ beta[j];
     
       promedio = promedio/151;

    cout<<"Promedio "; cout<<promedio;
    cout<<"\n";

//Desviación Estándar
      for (int j= 0; j<=150; j++)
       desviacion= desviacion + pow((beta[j]-promedio),2);
       
       desviacion=pow(desviacion,0.5)/pow(151,0.5);
       cout<<"la desviacion estandar es: "; cout<<desviacion;
       cout<<"\n";

//Valor Cuadrático Medio   
      for (int j= 0; j<=150; j++)
       rms= rms + pow(beta[j],2);
       
       rms=pow(rms,0.5)/pow(151,0.5);
       cout<<"El valor RMS: "; cout<<rms;
       cout<<"\n";

       fclose(fp);
    }
       system("PAUSE");
  }

Pero si quiero operar con todos los números en un archivo de texto sin importar cuántos sean, usaré este código:

#include <iostream.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#pragma hdrstop
#pragma argsused

void main(int argc, char* argv[])
{    FILE *fp;
     double i;
     double beta, c, promedio, rms, desviacion;
     char palabra[6];
     //valores estadísticos:
     promedio=0; desviacion=0; rms=0;

     i=0;
     fp = fopen("18.txt", "r");

if (fp==NULL)
 cout<<"no existe el archivo \n";

else {

    do
    {
       c = fscanf(fp, "%s", palabra);

       for(int j=0; j<6; j++)
       if (palabra[j]== ',')
       { palabra[j]='.'break; }
       
        beta = atof(palabra);
        cout<<beta;
        cout<<"\n";
        i++;

        promedio = promedio + beta;       
        rms = rms + pow(beta,2);

    }
  while (c != EOF);
     
    promedio = promedio/i;
    rms=pow(rms,0.5)/pow(i,0.5);

    i=0;
    rewind(fp);   

    do
    {
       c = fscanf(fp, "%s", palabra);

       for(int j=0; j<6; j++)
       if (palabra[j]== ',')
       { palabra[j]='.';  break; }
       
        beta = atof(palabra);
        i++;

        desviacion= desviacion + pow((beta-promedio),2);
    }
  while (c != EOF);

    desviacion=pow(desviacion,0.5)/pow(i,0.5);

    cout<<"Promedio "; cout<<promedio;
    cout<<"\n";
    cout<<"la desviacion estandar es: "; cout<<desviacion;
    cout<<"\n";
    cout<<"El valor RMS: "; cout<<rms;
    cout<<"\n";  

       fclose(fp);
    }
    system("PAUSE");
  }

Ambos códigos han sido probados con el compilador gratuito de Borland.
Cómo usar el compilador, lo explico aquí.

En este link hay información sobre "fscanf" y "rewind". Y aquí hay un tutorial sobre lectura y escritura de archivos con C++.

viernes, 15 de julio de 2011

Calculando la potencia de un número usando recursividad en C#

Primero, el código fuente:

    class Potencia
    {
        static void Main(string[] args)
        {
            Potencia pot = new Potencia();
            double r = pot.potenciaR(2, 1, 3, 0);
            Console.WriteLine(r.ToString());
            Console.Read();
        }

        private double potenciaR(double base0, double p, int exponente, int i)
        {
            return i >= exponente ? p : potenciaR(base0, p * base0, exponente, i + 1);
        }
    }

Los parámetros de la función "potenciaR" son: base, resultado, exponente, condición de parada.
El parámetro "p" debe ser 1 al inicio porque es el "acumulador" dodne se irá multiplicando la base tantas veces como indique el exponente. La función "potenciaR" deja de llamarse a sí misma cuando el parámetro "i" es igual al valor "exponente". El valor inicial de "i" es cero.

Y a continuación, lo mismo, pero con una expresión lambda recursiva:

class Potencia
    {
        static void Main(string[] args)
        {
            Func<double, double, double,double, double> potenciaF = null;
            potenciaF = (base0, p, exponente, i) =>
                    i >= exponente ? p : potenciaF(base0, p * base0, exponente, i + 1);

            Console.WriteLine(potenciaF(2, 1, 3, 0).ToString());
            Console.Read();
        }      
    }

Releyendo, noté que podemos suprimir el parámetro "i" y usar el mismo parámetro "exponente" como condición de parada. Para la expresión lambda recursiva queda así:

class Potencia
    {
        static void Main(string[] args)
        {
            Func<double, double, double, double> potenciaF = null;
            potenciaF = (base0, p, exponente) =>
                    exponente <= 0 ? p : potenciaF(base0, p * base0, exponente - 1);

            Console.WriteLine(potenciaF(2, 1, 3).ToString());
            Console.Read();
        }
    }

En todos los casos, el parámetro "p" es siempre 1.

Esta es la web donde aprendí a hacer expresiones lambda recursivas:
http://commanet.blogspot.com/2008/02/recursividad-annima.html

viernes, 3 de junio de 2011

VB6 Code Snippet: Mantener la proporción de alto y ancho de una imagen

Tengo una imagen cargada en un control Image en Visual Basic 6. Quiero que esa imagen mantenga su proporción Ancho/Alto cuando el control Image tiene su propiedad Stretch en True.  El código para hacerlo es el siguiente (He llamado al control Image imgProporcion):

        Dim i As Double, j As Double
        Dim proporcion As Double
       
        i = imgProporcion.Width
        j = imgProporcion.Height
   
        imgProporcion.Visible = False
        imgProporcion.Stretch = False
               
        i = (i / imgProporcion.Width)
        j = (j / imgProporcion.Height)
       
        If j > i Then proporcion = j Else proporcion = i
       
        imgProporcion.Stretch = True
        imgProporcion.Width = CInt(proporcion * imgProporcion.Width)
        imgProporcion.Height = CInt(proporcion * imgProporcion.Height)
        imgProporcion.Visible = True

viernes, 29 de abril de 2011

Haciendo el Test Fizz Buzz con Linq en C#

Leyendo algunos de los muchos blogs de programación flotando por ahí, es muy probable toparse con el Test Fizz Buzz, el que yo ya he usado antes para practicar algunas cosas de programación en C#.
Y por ahí se me ocurrió hacerlo usando Linq. Todo el código es el siguiente:

using System;
using System.Linq;
using System.Collections.Generic;

namespace Operator
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] fbz = new int[100];

            for (int i = 1; i <= 100; i++)
                fbz[i - 1] = i;

            IEnumerable<string> fizbuz = fbz.Select(           
                c=> (c % 3 == 0) & (c % 5 == 0) ? "FizzBuzz" :
                    (c % 3 == 0) ? "Fizz" :
                    (c % 5 == 0) ? "Buzz" : c.ToString() );

            foreach (string c in fizbuz)
                Console.WriteLine(c);

            Console.Read();
        }
    }
}

Una cosa interesante del Linq es que tiene algo llamado "Lazy Evaluation" ó "Evaluación perezosa": la ejecución de las instrucciones no se realiza cuando se declara la interfaz IEnumerable "fizbuz", si no cuando se la recorre con el bucle foreach. De esta forma se pueden tener muchas interfaces pertenecientes a System.Collections y recién ejecutar sus códigos cuando se las recorre con un foreach.
Y luego, para cumplir con las condiciones del test, vuelvo a usar el operador ternario "?", el cual me gusta mucho porque marea cuando se lo ve las primeras veces :D :D

martes, 19 de abril de 2011

C# Code Snippet: Usando el control FontDialog

En una aplicación tipo WindowsApplication tengo un FontDialog, y quiero que cuando el usuario hace click en "Aceptar", los valores de este FontDialog se almacenen en un objeto tipo FontStyle, el que luego se utiliza para crear un objeto tipo Font.

El código es éste:

if (dlgFont.ShowDialog() == DialogResult.OK)
{
  // asigno el estilo de fuente
    myFontStyle =
        dlgFont.Font.Bold ? FontStyle.Bold :
        dlgFont.Font.Italic ? FontStyle.Italic :
        dlgFont.Font.Underline ? FontStyle.Underline :
        dlgFont.Font.Strikeout ? FontStyle.Strikeout : FontStyle.Regular;

    myFont = 
         new Font(dlgFont.Font.Name, dlgFont.Font.Size, myFontStyle);                                          
}

Aquí hago uso del operador ternario '?' para acortar la expresión donde se asigna el tipo de la fuente. Si no uso este operador, la expresión sería:

if (dlgFont.Font.Bold)
    myFontStyle = FontStyle.Bold;
else
    if (dlgFont.Font.Italic)
        myFontStyle = FontStyle.Italic;
    else
        if (dlgFont.Font.Underline)
            myFontStyle = FontStyle.Underline;
        else
            if (dlgFont.Font.Strikeout)
                myFontStyle = FontStyle.Strikeout;
            else
                myFontStyle = FontStyle.Regular; 

jueves, 24 de marzo de 2011

VB6: Rutina para Eliminar FIlas Repetidas en un Hierarchical FlexGrid

Esta rutina está pensada para un Hierarchical FlexGrid cuya primera fila (la que tiene el índice 0) contiene los nombres de las columnas, por ello procesa a partir de la fila con índice 1:

Sub eliminar_Repetidos()
Dim estaFila, unaFila As String
Dim i, j, m, n As Integer

estaFila = vbNullString
unaFila = vbNullString

With HierarchicalFlexGrid
    m = 1 ' Apunta a la primera Fila
    n = .Rows - 1 ' Cantidad de filas del HierarchicalFlexGrid
   
    Do Until m > n
        estaFila = vbNullString  'cadena con la Fila Actual
       
        For i = 0 To .Cols - 1
            estaFila = estaFila + .TextMatrix(m, i) ' Relleno la cadena con los datos de toda la fila
        Next
       
        j = 1
        Do Until j > n
            unaFila = vbNullString ' Fila a comparar
           
            For i = 0 To .Cols - 1
                unaFila = unaFila + .TextMatrix(j, i) ' la relleno con los datos de toda la fila
            Next
           
            If estaFila = unaFila And j <> m Then
                .RemoveItem (j) ' Si son iguales remuevo unaFila solamente
                n = n - 1 ' tamaño actual del HierarchicalFlexgrid, al remover disminuye en uno
            End If
           
            j = j + 1
        Loop
        m = m + 1
    Loop
   
    'Chequeo si la última y la penúltima fila son iguales, porque no se eliminan si lo son
    For i = 0 To .Cols - 1
        estaFila = estaFila + .TextMatrix(.Rows - 1, i)
        unaFila = unaFila + .TextMatrix(.Rows - 2, i)
    Next
   
    If unaFila = estaFila Then .RemoveItem (.Rows - 2)
End With
End Sub


La rutina hace lo siguiente: cojo la primera fila y, en un bucle, la comparo con todas las demás y voy eliminando las que son iguales a la primera fila. Al terminar paso a la segunda fila y vuelvo a comparar y eliminar si son iguales. Esto se repite hasta llegar al final del Hierarchical FlexGrid.
Este algoritmo es más eficiente que, en dos bucles anidados, marcar las filas repetidas para eliminarlas después.

viernes, 11 de marzo de 2011

Obtener los nombres de las tablas y los nombres de sus esquemas de una BD en SQLServer desde VB6

Un día intentaba conectarme a AdventureWorks sample BD desde una app en Visual Basic 6.

Declaraba un recordset como sigue:

db.Open miConnectionString 'db es un objeto tipo Connection
Set rs = db.OpenSchema(adSchemaTables, Array(Empty, Empty, Empty, "TABLE"))
para poder listar todas sus tablas, usando un bucle.
Mi problema era que al ejecutar un query en SQL:

Dim s as String

Do Until rs.EOF
s="Select * from " & rs!Table_Name
' Hacer muchas muchas cosas con la variable "s"
rs.MoveNext
Loop

Me botaba error, ya que sólo tenía el nombre de la tabla. Para que funcionara, el query dentro de la variable "s" necessitaba también el nombre del esquema (Schema Name) y contruirlo todo de la siguiente forma:
Select * from Schema_Name.Tabla

Luego de mucho rebuscar en Google, hallé esta web
http://www.techrepublic.com/article/much-ado-about-field-properties/1045310

Donde explican lo que hace OpenSchema. Para mi caso, utilizo el SchemaID "AdSchemaTables", el cual incluye un filtro llamado "TABLE_SCHEMA".

Finalmente mi query queda:
Dim s as String

Do Until rs.EOF
s="Select * from " & rs!Table_Schema & "." & rs!Table_Name
' Hacer muchas muchas cosas con la variable "s" 
rs.MoveNext
Loop

Update!
Cómo saber si una tabla tiene un nombre de esquema asignado:
Basta añadir este código en el bucle ("miTabla" es una variable tipo String):

miTabla = vbNullString
miTabla = rs!TABLE_SCHEMA
    
If miTabla <> vbNullString Then
      miTabla = rs!TABLE_SCHEMA & "." & rs!TABLE_NAME
Else
      miTabla = rs!TABLE_NAME
End If 

jueves, 17 de febrero de 2011

C# y Ms Access: Excepciòn del tipo "Insert Into Syntaxis Error"

Sucedió un día que tenía que insertar unos datitos a una base de datos en Access 2003 usando C#. La base de datos tiene una sola tabla (la llamaré "miTabla") con tres campos: "No", "un_ID" y "Tittle" de tipos numérico, texto y texto, respectivamente.

Mi código era:

private void AddDato(string No, string ID, string Tittle, ref OleDbConnection cnn)
        {
            OleDbCommand cmd = new OleDbCommand(
"INSERT INTO miTabla (No, un_ID, Tittle) VALUES(@Num, @uID, @nTittle)", cnn);
            cmd.Parameters.AddWithValue("@Num", Convert.ToInt32(No));
            cmd.Parameters.AddWithValue("@uID", ID);
            cmd.Parameters.AddWithValue("@nTittle", Tittle);          

                cnn.Open();
                cmd.ExecuteNonQuery();

                cmd.Dispose();
                cnn.Close();
            }
        }


Y a pesar de estar todo bien, siempre me tiraba una excepción que decía "Syntaxis Error en Insert Into", o cualquier cosa parecida, en la línea cmd.ExecuteNonQuery().

Después de horas depurando y leyendo foros, encontré por ahí un tip clave:
A las bases de datos en Access no les gustan los campos con nombres como "Date", "login" o "Password".

Entonces el error no estaba en mi Insert Into, si no, posiblemente, en los nombres de mis campos.
Hice una pequeña modificación al código (no a la base de datos) y el problema se resolvió maravillosamente:

private void AddDato(string No, string ID, string Tittle, ref OleDbConnection cnn)
        {
            OleDbCommand cmd = new OleDbCommand(
"INSERT INTO miTabla ([No], [un_ID], [Tittle]) VALUES(@Num, @uID, @nTittle)", cnn);
            cmd.Parameters.AddWithValue("@Num", Convert.ToInt32(No));
            cmd.Parameters.AddWithValue("@uID", ID);
            cmd.Parameters.AddWithValue("@nTittle", Tittle);

                cnn.Open();
                cmd.ExecuteNonQuery();


                cmd.Dispose();
                cnn.Close();
            }
        }
 

Dejo como tarea hallar el cambio que hice :)

martes, 18 de enero de 2011

C# usando LastIndexOf y Substring para separar el nombre de un archivo de la ruta donde está almacenado

Ya antes hice lo mismo en VB6. Ahora toca hacerlo desde C#.

Imaginemos que tengo este string que contiene la ruta a un archivo:

unaRuta = "C:\unaCarpeta\otraCarpeta\unArchivo.txt"

Y quisiera separalo en dos strings, con la ruta del archivo y el nombre del archivo por separado:

miRuta =  "C:\unaCarpeta\otraCarpeta\"
miArchivo = "unArchivo.txt" 

El código es el siguiente:


int n = unaRuta.LastIndexOf(@"\");  // la @ es para que considere el "\" como caracter
// se extrae la subcadena desde la primera letra hasta n+1
string miRuta = unaRuta.Substring(0, n + 1);
// se extrae la subcadena desde la letra n+1 hasta el final
string miArchivo = unaRuta.Substring(n + 1);